From 8705c4fe35dc8d7eead0ac4778fda528c697a0ec Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Apr 2018 16:27:13 +0100 Subject: [PATCH 01/27] Fix Doc.prototype.destroy The problem was that unsubscribe re-added the doc to the connection. Now the doc is removed from the connection after unsubscribe. Additionally, we're no longer waiting for the unsubscribe response before executing the callback. It is consistent with Query, unsubscribe can't fail anyway and the subscribed state is updated synchronously on the client side. --- lib/client/doc.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 05e17976d..bf128eb51 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -104,10 +104,8 @@ emitter.mixin(Doc); Doc.prototype.destroy = function(callback) { var doc = this; doc.whenNothingPending(function() { + if (doc.wantSubscribe) doc.unsubscribe(); doc.connection._destroyDoc(doc); - if (doc.wantSubscribe) { - return doc.unsubscribe(callback); - } if (callback) callback(); }); }; From af84be65f208856e0933d627464b79ab053a6dc0 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Apr 2018 16:36:58 +0100 Subject: [PATCH 02/27] Update tested nodejs versions in .travis.yml See See https://github.com/nodejs/Release --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b9165051..66e0be28c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: node_js node_js: - - 6 - - 5 - - 4 - - 0.10 + - "9" + - "8" + - "6" + - "4" script: "npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From 09edf920eedf6d5116efb2271d693a9a59da1517 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Apr 2018 12:37:24 +0100 Subject: [PATCH 03/27] Add a test --- lib/client/doc.js | 4 +++- test/client/subscribe.js | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index bf128eb51..33621cb9c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -104,7 +104,9 @@ emitter.mixin(Doc); Doc.prototype.destroy = function(callback) { var doc = this; doc.whenNothingPending(function() { - if (doc.wantSubscribe) doc.unsubscribe(); + if (doc.wantSubscribe) { + doc.unsubscribe(); + } doc.connection._destroyDoc(doc); if (callback) callback(); }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 567031d0a..db2bea1b2 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -405,8 +405,10 @@ describe('client subscribe', function() { }); it('doc destroy stops op updates', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + var doc = connection1.get('dogs', 'fido'); + var doc2 = connection2.get('dogs', 'fido'); doc.create({age: 3}, function(err) { if (err) return done(err); doc2.subscribe(function(err) { @@ -416,6 +418,7 @@ describe('client subscribe', function() { }); doc2.destroy(function(err) { if (err) return done(err); + expect(connection2.getExisting('dogs', 'fido')).equal(undefined); doc.submitOp({p: ['age'], na: 1}, done); }); }); From 9f843efc23f9c07c9dc3c2bb1d43a67955c16a79 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 21 Jun 2018 17:35:48 +0100 Subject: [PATCH 04/27] Implement undo/redo --- .editorconfig | 9 + .travis.yml | 1 - README.md | 42 +- lib/client/doc.js | 353 +++++++- package-lock.json | 1528 ++++++++++++++++++++++++++++++++ package.json | 3 + test/client/invertible-type.js | 80 ++ test/client/undo-redo.js | 1376 ++++++++++++++++++++++++++++ 8 files changed, 3371 insertions(+), 21 deletions(-) create mode 100644 .editorconfig create mode 100644 package-lock.json create mode 100644 test/client/invertible-type.js create mode 100644 test/client/undo-redo.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e29f5e504 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = LF +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.travis.yml b/.travis.yml index 7bd066b20..5c87e1e6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ node_js: - "9" - "8" - "6" - - "4" script: "npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/README.md b/README.md index 12d84d744..2023a0908 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ tracker](https://github.com/share/sharedb/issues). - Realtime synchronization of any JSON document - Concurrent multi-user collaboration +- Local undo and redo - Synchronous editing API with asynchronous eventual consistency - Realtime query subscriptions - Simple integration with any database - [MongoDB](https://github.com/share/sharedb-mongo), [PostgresQL](https://github.com/share/sharedb-postgres) (experimental) @@ -214,7 +215,7 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance. ### Class: `ShareDB.Doc` -`doc.type` _(String_) +`doc.type` _(String)_ The [OT type](https://github.com/ottypes/docs) of this document `doc.id` _(String)_ @@ -223,6 +224,13 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. +`doc.undoLimit` _(Number, read-write, default=100)_ +The max number of operations to keep on the undo stack. + +`doc.undoComposeTimeout` _(Number, read-write, default=1000)_ +The max time difference between operations in milliseconds, +which still allows "UNDOABLE" operations to be composed on the undo stack. + `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. @@ -246,8 +254,8 @@ 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('op', function(op, source, operationType) {...})` +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. `operationType` is one of the following: `"UNDOABLE"` _(local operation that can be undone)_, `"FIXED"` _(local or remote operation that can't be undone nor redone)_, `"UNDO"` _(local undo operation that can be redone)_ and `"REDO"` _(local redo operation that can be undone)_. `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. @@ -271,6 +279,19 @@ Apply operation to document and send it to the server. [operations for the default `'ot-json0'` type](https://github.com/ottypes/json0#summary-of-operations). Call this after you've either fetched or subscribed to the document. * `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. +* `options.undoable` Should it be possible to undo this operation, default=false. +* `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. +* `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. + +`doc.submitSnapshot(snapshot[, options][, function(err) {...}])` +Diff the current and the provided snapshots to generate an operation, apply the operation to the document and send it to the server. +`snapshot` structure depends on the document type. +Call this after you've either fetched or subscribed to the document. +* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. +* `options.undoable` Should it be possible to undo this operation, default=false. +* `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. +* `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. +* `options.diffHint` A hint passed into the `diff`/`diffX` functions defined by the document type. `doc.del([options][, function(err) {...}])` Delete the document locally and send delete operation to the server. @@ -285,6 +306,20 @@ Invokes the given callback function after Note that `whenNothingPending` does NOT wait for pending `model.query()` calls. +`doc.canUndo()` +Return `true`, if there's an operation on the undo stack that can be undone, otherwise `false`. + +`doc.undo([options][, function(err) {...}])` +Undo a previously applied "UNDOABLE" or "REDO" operation. +* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. + +`doc.canRedo()` +Return `true`, if there's an operation on the redo stack that can be undone, otherwise `false`. + +`doc.redo([options][, function(err) {...}])` +Redo a previously applied "UNDO" operation. +* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. + ### Class: `ShareDB.Query` `query.ready` _(Boolean)_ @@ -360,6 +395,7 @@ Additional fields may be added to the error object for debugging context dependi * 4021 - Invalid client id * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type +* 4024 - OT Type does not support `diff` nor `diffX` ### 5000 - Internal error diff --git a/lib/client/doc.js b/lib/client/doc.js index 05e17976d..c6b9fa5bb 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -44,6 +44,13 @@ var types = require('../types'); * - `load ()` Fired when a new snapshot is ingested from a fetch, subscribe, or query */ +var OPERATION_TYPES = { + UNDOABLE: 'UNDOABLE', // basic operation that can be undone + FIXED: 'FIXED', // basic operation that cannot be undone + UNDO: 'UNDO', // undo operation + REDO: 'REDO' // redo operation +}; + module.exports = Doc; function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -57,6 +64,19 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; + // Undo stack for local operations. + this.undoStack = []; + // Redo stack for local operations. + this.redoStack = []; + // The max number of undo operations to keep on the stack. + this.undoLimit = 100; + // The max time difference between operations in milliseconds, + // which still allows the operations to be composed on the undoStack. + this.undoComposeTimeout = 1000; + // The timestamp of the previous reversible operation. Used to determine if + // the next reversible operation can be composed on the undoStack. + this.previousUndoableOperationTime = -Infinity; + // Array of callbacks or nulls as placeholders this.inflightFetch = []; this.inflightSubscribe = []; @@ -191,6 +211,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.data = (this.type && this.type.deserialize) ? this.type.deserialize(snapshot.data) : snapshot.data; + this._clearUndoRedo(); this.emit('load'); callback && callback(); }; @@ -318,7 +339,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._otApply(message, false); + this._otApply(message); return; }; @@ -500,7 +521,8 @@ function transformX(client, server) { * * @private */ -Doc.prototype._otApply = function(op, source) { +Doc.prototype._otApply = function(op, options) { + var source = options && options.source || false; if (op.op) { if (!this.type) { var err = new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); @@ -537,8 +559,8 @@ Doc.prototype._otApply = function(op, source) { } // Apply the individual op component this.emit('before op', componentOp.op, source); - this.data = this.type.apply(this.data, componentOp.op); - this.emit('op', componentOp.op, source); + this._applyOp(componentOp, options); + this.emit('op', componentOp.op, source, 'FIXED'); } // Pop whatever was submitted since we started applying this op this._popApplyStack(stackLength); @@ -549,13 +571,13 @@ Doc.prototype._otApply = function(op, source) { // 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); + var operationType = this._applyOp(op, options); // Emit an '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('op', op.op, source, operationType); return; } @@ -566,6 +588,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); + this._clearUndoRedo(); this.emit('create', source); return; } @@ -573,11 +596,167 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); + this._clearUndoRedo(); this.emit('del', oldData, source); return; } }; +// Applies `op` to `this.data` and updates the undo/redo stacks. +Doc.prototype._applyOp = function(op, options) { + var undoOp = options && options.undoOp || null; + var operationType = options && options.operationType || OPERATION_TYPES.FIXED; + var fixUpUndoStack = options && options.fixUpUndoStack; + var fixUpRedoStack = options && options.fixUpRedoStack; + var needsUndoOp = operationType !== OPERATION_TYPES.FIXED || fixUpUndoStack || fixUpRedoStack; + + if (needsUndoOp && undoOp == null) { + if (this.type.applyAndInvert) { + var result = this.type.applyAndInvert(this.data, op.op); + this.data = result[0]; + undoOp = { op: result[1] }; + + } else if (this.type.invert) { + this.data = this.type.apply(this.data, op.op); + undoOp = { op: this.type.invert(op.op) }; + + } else { + this.data = this.type.apply(this.data, op.op); + operationType = OPERATION_TYPES.FIXED; + } + + } else { + this.data = this.type.apply(this.data, op.op); + } + + switch (operationType) { + case OPERATION_TYPES.UNDOABLE: + this._updateStacksUndoable(op, undoOp); + break; + case OPERATION_TYPES.UNDO: + this._updateStacksUndo(op, undoOp); + break; + case OPERATION_TYPES.REDO: + this._updateStacksRedo(op, undoOp); + break; + default: + this._updateStacksFixed(op, undoOp, fixUpUndoStack, fixUpRedoStack); + break; + }; + + return operationType; +}; + +Doc.prototype._clearUndoRedo = function() { + this.undoStack.length = 0; + this.redoStack.length = 0; + this.previousUndoableOperationTime = -Infinity; +}; + +Doc.prototype._updateStacksUndoable = function(op, undoOp) { + var now = Date.now(); + + if (this.undoStack.length === 0 || now - this.previousUndoableOperationTime > this.undoComposeTimeout) { + this.undoStack.push(undoOp); + + } else if (this.type.composeSimilar) { + var lastOp = this.undoStack.pop(); + var composedOp = this.type.composeSimilar(undoOp.op, lastOp.op); + if (composedOp != null) { + this.undoStack.push({ op: composedOp }); + } else { + this.undoStack.push(lastOp, undoOp); + } + + } else if (this.type.compose) { + var lastOp = this.undoStack.pop(); + var composedOp = this.type.compose(undoOp.op, lastOp.op); + this.undoStack.push({ op: composedOp }); + + } else { + this.undoStack.push(undoOp); + } + + this.redoStack.length = 0; + this.previousUndoableOperationTime = now; + + var isNoop = this.type.isNoop; + if (isNoop && isNoop(this.undoStack[this.undoStack.length - 1].op)) { + this.undoStack.pop(); + } + + var itemsToRemove = this.undoStack.length - this.undoLimit; + if (itemsToRemove > 0) { + this.undoStack.splice(0, itemsToRemove); + } +}; + +Doc.prototype._updateStacksUndo = function(op, undoOp) { + if (!this.type.isNoop || !this.type.isNoop(undoOp.op)) { + this.redoStack.push(undoOp); + } + this.previousUndoableOperationTime = -Infinity; +}; + +Doc.prototype._updateStacksRedo = function(op, undoOp) { + if (!this.type.isNoop || !this.type.isNoop(undoOp.op)) { + this.undoStack.push(undoOp); + } + this.previousUndoableOperationTime = -Infinity; +}; + +Doc.prototype._updateStacksFixed = function(op, undoOp, fixUpUndoStack, fixUpRedoStack) { + if (fixUpUndoStack && this.undoStack.length > 0 && this.type.compose && undoOp) { + var lastOp = this.undoStack.pop(); + var composedOp = this.type.compose(undoOp.op, lastOp.op); + if (!this.type.isNoop || !this.type.isNoop(composedOp)) { + this.undoStack.push({ op: composedOp }); + } + } else { + this.undoStack = this._transformStack(this.undoStack, op.op); + } + + if (fixUpRedoStack && this.redoStack.length > 0 && this.type.compose && undoOp) { + var lastOp = this.redoStack.pop(); + var composedOp = this.type.compose(undoOp.op, lastOp.op); + if (!this.type.isNoop || !this.type.isNoop(composedOp)) { + this.redoStack.push({ op: composedOp }); + } + } else { + this.redoStack = this._transformStack(this.redoStack, op.op); + } +}; + +Doc.prototype._transformStack = function(stack, op) { + var transform = this.type.transform; + var transformX = this.type.transformX; + var isNoop = this.type.isNoop; + var newStack = []; + var newStackIndex = 0; + + for (var i = stack.length - 1; i >= 0; --i) { + var stackOp = stack[i].op; + var transformedStackOp; + var transformedOp; + + if (transformX) { + var result = transformX(op, stackOp); + transformedOp = result[0]; + transformedStackOp = result[1]; + } else { + transformedOp = transform(op, stackOp, 'left'); + transformedStackOp = transform(stackOp, op, 'right'); + } + + if (!isNoop || !isNoop(transformedStackOp)) { + newStack[newStackIndex++] = { op: transformedStackOp }; + } + + op = transformedOp; + } + + return newStack.reverse(); +}; // ***** Sending operations @@ -630,10 +809,14 @@ Doc.prototype._sendOp = function() { // @param [op.op] // @param [op.del] // @param [op.create] +// @param options { source, operationType, undoOp, fixUpUndoStack, fixUpRedoStack } // @param [callback] called when operation is submitted -Doc.prototype._submit = function(op, source, callback) { +Doc.prototype._submit = function(op, options, callback) { + if (!options) options = {}; + if (!options.operationType) options.operationType = OPERATION_TYPES.FIXED; + // Locally submitted ops must always have a truthy source - if (!source) source = true; + if (!options.source) options.source = true; // The op contains either op, create, delete, or none of the above (a no-op). if (op.op) { @@ -644,10 +827,15 @@ Doc.prototype._submit = function(op, source, callback) { } // Try to normalize the op. This removes trailing skip:0's and things like that. if (this.type.normalize) op.op = this.type.normalize(op.op); + // Try to skip processing empty operations. + if (this.type.isNoop && this.type.isNoop(op.op)) { + if (callback) process.nextTick(callback); + return; + } } this._pushOp(op, callback); - this._otApply(op, source); + this._otApply(op, options); // The call to flush is delayed so if submit() is called multiple times // synchronously, all the ops are combined before being sent to the server. @@ -733,19 +921,150 @@ Doc.prototype._tryCompose = function(op) { // Submit an operation to the document. // -// @param operation handled by the OT type -// @param options {source: ...} +// @param component operation handled by the OT type +// @param options.source passed into 'op' event handler +// @param options.undoable should the operation be undoable +// @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. +// If false (default), op transforms undoStack. +// If true, op is inverted and composed into the last operation on the undoStack. +// @param options.fixUpRedoStack Determines how non-undoable op affects redoStack. +// If false (default), op transforms redoStack. +// If true, op is inverted and composed into the last operation on the redoStack. // @param [callback] called after operation submitted // -// @fires before op, op, after op +// @fires before op, op Doc.prototype.submitOp = function(component, options, callback) { if (typeof options === 'function') { callback = options; options = null; } var op = {op: component}; - var source = options && options.source; - this._submit(op, source, callback); + var submitOptions = { + source: options && options.source, + operationType: options && options.undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, + fixUpUndoStack: options && options.fixUpUndoStack, + fixUpRedoStack: options && options.fixUpRedoStack + }; + this._submit(op, submitOptions, callback); +}; + +// Submits new content for the document. +// +// This function works only if the type supports `diff` or `diffX`. +// It diffs the current and new snapshot to generate an operation, +// which is then submitted as usual. +// +// @param snapshot new snapshot data +// @param options.source passed into 'op' event handler +// @param options.undoable should the operation be undoable +// @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. +// If false (default), op transforms undoStack. +// If true, op is inverted and composed into the last operation on the undoStack. +// @param options.fixUpRedoStack Determines how non-undoable op affects redoStack. +// If false (default), op transforms redoStack. +// If true, op is inverted and composed into the last operation on the redoStack. +// @param options.diffHint a hint passed into diff/diffX +// @param [callback] called after operation submitted + +// @fires before op, op +Doc.prototype.submitSnapshot = function(snapshot, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!this.type) { + var err = new ShareDBError(4015, 'Cannot submit snapshot. Document has not been created. ' + this.collection + '.' + this.id); + if (callback) return callback(err); + return this.emit('error', err); + } + if (!this.type.diff && !this.type.diffX) { + var err = new ShareDBError(4024, 'Cannot submit snapshot. Document type does not support diff nor diffX. ' + this.collection + '.' + this.id); + if (callback) return callback(err); + return this.emit('error', err); + } + + var undoable = !!(options && options.undoable); + var fixUpUndoStack = options && options.fixUpUndoStack; + var fixUpRedoStack = options && options.fixUpRedoStack; + var diffHint = options && options.diffHint; + var needsUndoOp = undoable || fixUpUndoStack || fixUpRedoStack; + var op, undoOp; + + if ((needsUndoOp && this.type.diffX) || !this.type.diff) { + var diffs = this.type.diffX(this.data, snapshot, diffHint); + undoOp = { op: diffs[0] }; + op = { op: diffs[1] }; + } else { + undoOp = null; + op = { op: this.type.diff(this.data, snapshot, diffHint) }; + } + + var submitOptions = { + source: options && options.source, + operationType: undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, + undoOp: undoOp, + fixUpUndoStack: fixUpUndoStack, + fixUpRedoStack: fixUpRedoStack + }; + this._submit(op, submitOptions, callback); +}; + +// Returns true, if there are any operations on the undo stack, otherwise false. +Doc.prototype.canUndo = function() { + return this.undoStack.length > 0 +}; + +// Undoes a submitted operation. +// +// @param options {source: ...} +// @param [callback] called after operation submitted +// @fires before op, op +Doc.prototype.undo = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!this.canUndo()) { + if (callback) process.nextTick(callback); + return; + } + + var op = this.undoStack.pop(); + var submitOptions = { + source: options && options.source, + operationType: OPERATION_TYPES.UNDO + }; + this._submit(op, submitOptions, callback); +}; + +// Returns true, if there are any operations on the redo stack, otherwise false. +Doc.prototype.canRedo = function() { + return this.redoStack.length > 0 +}; + +// Redoes an undone operation. +// +// @param options {source:...} +// @param [callback] called after operation submitted +// @fires before op, op +Doc.prototype.redo = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!this.canRedo()) { + if (callback) process.nextTick(callback); + return; + } + + var op = this.redoStack.pop() + var submitOptions = { + source: options && options.source, + operationType: OPERATION_TYPES.REDO + }; + this._submit(op, submitOptions, callback); }; // Create the document, which in ShareJS semantics means to set its type. Every @@ -776,7 +1095,7 @@ Doc.prototype.create = function(data, type, options, callback) { } var op = {create: {type: type, data: data}}; var source = options && options.source; - this._submit(op, source, callback); + this._submit(op, { source: source }, callback); }; // Delete the document. This creates and submits a delete operation to the @@ -798,7 +1117,7 @@ Doc.prototype.del = function(options, callback) { } var op = {del: true}; var source = options && options.source; - this._submit(op, source, callback); + this._submit(op, { source: source }, callback); }; @@ -858,7 +1177,7 @@ Doc.prototype._rollback = function(err) { // I'm still not 100% sure about this functionality, because its really a // local op. Basically, the problem is that if the client's op is rejected // by the server, the editor window should update to reflect the undo. - this._otApply(op, false); + this._otApply(op); this._clearInflightOp(err); return; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..0af5c9b23 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1528 @@ +{ + "name": "sharedb", + "version": "1.0.0-beta.9", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@teamwork/ot-rich-text": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@teamwork/ot-rich-text/-/ot-rich-text-6.3.3.tgz", + "integrity": "sha512-cYHZPTRMY6N7GxJ3SENzHyGVtgLlDfMdtfRs1CG+6+6DzUkfP/VkEIoR+K5jp02DyI5yvMErkyJkv4BvM23sLg==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arraydiff": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", + "integrity": "sha1-hqVDbXty8b3aX9bXTock5C+Dzk0=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "coveralls": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", + "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", + "dev": true, + "requires": { + "js-yaml": "3.6.1", + "lcov-parse": "0.0.10", + "log-driver": "1.2.5", + "minimist": "1.2.0", + "request": "2.79.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "optional": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hat": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jshint": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz", + "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=", + "dev": true, + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "3.7.x", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz", + "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "~1.33.0" + } + }, + "mingo": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mingo/-/mingo-2.2.2.tgz", + "integrity": "sha1-vmnUhq5uCsVLl53F9EEtshhR9pM=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", + "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "ot-json0": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", + "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" + }, + "ot-text": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ot-text/-/ot-text-1.0.1.tgz", + "integrity": "sha1-P4UPbuhYvDbvRayapR0Gx354388=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "quill-delta": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.2.tgz", + "integrity": "sha512-grWEQq9woEidPDogtDNxQKmy2LFf9zBC0EU/YTSw6TwKmMjtihTxdnPtPRfrqazB2MSJ7YdCWxmsJ7aQKRSEgg==", + "dev": true, + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.1", + "fast-diff": "1.1.2" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "qs": "~6.3.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1", + "uuid": "^3.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "rich-text": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rich-text/-/rich-text-3.1.0.tgz", + "integrity": "sha1-BMlx3tzo64IBDPrP9uegzjXHqCU=", + "dev": true, + "requires": { + "quill-delta": "^3.2.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sharedb": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-1.0.0-beta.9.tgz", + "integrity": "sha1-LX20J83hIJJNLasIzpLZq6134As=", + "dev": true, + "requires": { + "arraydiff": "^0.1.1", + "async": "^1.4.2", + "deep-is": "^0.1.3", + "hat": "0.0.3", + "make-error": "^1.1.1", + "ot-json0": "^1.0.1" + } + }, + "sharedb-mingo-memory": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sharedb-mingo-memory/-/sharedb-mingo-memory-1.0.0.tgz", + "integrity": "sha1-vS5171YTCrheE5uMlMVSFv4+TQM=", + "dev": true, + "requires": { + "mingo": "^2.2.0", + "sharedb": "^1.0.0-beta" + } + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json index 35fc64bc6..4b96159f5 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,14 @@ "ot-json0": "^1.0.1" }, "devDependencies": { + "@teamwork/ot-rich-text": "^6.3.3", "coveralls": "^2.11.8", "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", "mocha": "^3.2.0", + "ot-text": "^1.0.1", + "rich-text": "^3.1.0", "sharedb-mingo-memory": "^1.0.0-beta" }, "scripts": { diff --git a/test/client/invertible-type.js b/test/client/invertible-type.js new file mode 100644 index 000000000..34a4f3fd6 --- /dev/null +++ b/test/client/invertible-type.js @@ -0,0 +1,80 @@ +// A simple type for testing undo/redo, where: +// +// - snapshot is an integer +// - operation is an integer +exports.type = { + name: 'invertible-type', + uri: 'http://sharejs.org/types/invertible-type', + create: create, + apply: apply, + transform: transform, + invert: invert +}; + +exports.typeWithDiff = { + name: 'invertible-type-with-diff', + uri: 'http://sharejs.org/types/invertible-type-with-diff', + create: create, + apply: apply, + transform: transform, + invert: invert, + diff: diff +}; + +exports.typeWithDiffX = { + name: 'invertible-type-with-diffX', + uri: 'http://sharejs.org/types/invertible-type-with-diffX', + create: create, + apply: apply, + transform: transform, + invert: invert, + diffX: diffX +}; + +exports.typeWithDiffAndDiffX = { + name: 'invertible-type-with-diff-and-diffX', + uri: 'http://sharejs.org/types/invertible-type-with-diff-and-diffX', + create: create, + apply: apply, + transform: transform, + invert: invert, + diff: diff, + diffX: diffX +}; + +exports.typeWithTransformX = { + name: 'invertible-type-with-transformX', + uri: 'http://sharejs.org/types/invertible-type-with-transformX', + create: create, + apply: apply, + transformX: transformX, + invert: invert +}; + +function create(data) { + return data | 0; +} + +function apply(snapshot, op) { + return snapshot + op; +} + +function transform(op1, op2, side) { + return op1; +} + +function transformX(op1, op2) { + return [ op1, op2 ]; +} + +function invert(op) { + return -op; +} + +function diff(oldSnapshot, newSnapshot) { + return newSnapshot - oldSnapshot; +} + +function diffX(oldSnapshot, newSnapshot) { + return [ oldSnapshot - newSnapshot, newSnapshot - oldSnapshot ]; +} diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js new file mode 100644 index 000000000..6ba6a9866 --- /dev/null +++ b/test/client/undo-redo.js @@ -0,0 +1,1376 @@ +var async = require('async'); +var util = require('../util'); +var errorHandler = util.errorHandler; +var Backend = require('../../lib/backend'); +var ShareDBError = require('../../lib/error'); +var expect = require('expect.js'); +var types = require('../../lib/types'); +var otText = require('ot-text'); +var otRichText = require('@teamwork/ot-rich-text'); +var invertibleType = require('./invertible-type'); + +types.register(otText.type); +types.register(otRichText.type); +types.register(invertibleType.type); +types.register(invertibleType.typeWithDiff); +types.register(invertibleType.typeWithDiffX); +types.register(invertibleType.typeWithDiffAndDiffX); +types.register(invertibleType.typeWithTransformX); + +describe('client undo/redo', function() { + beforeEach(function() { + this.backend = new Backend(); + this.connection = this.backend.connect(); + this.connection2 = this.backend.connect(); + this.doc = this.connection.get('dogs', 'fido'); + this.doc2 = this.connection2.get('dogs', 'fido'); + }); + + afterEach(function(done) { + this.backend.close(done); + }); + + it('submits a fixed operation', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ]), + function(done) { + expect(this.doc.version).to.equal(2); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('receives a remote operation', function(allDone) { + async.series([ + this.doc.subscribe.bind(this.doc), + this.doc2.create.bind(this.doc2, { test: 5 }), + this.doc2.submitOp.bind(this.doc2, [ { p: [ 'test' ], na: 2 } ]), + setTimeout, + function(done) { + expect(this.doc.version).to.equal(2); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('submits an undoable operation', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + function(done) { + expect(this.doc.version).to.equal(2); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('undoes an operation', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(3); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('redoes an operation', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(4); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('performs a series of undo and redo operations', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(8); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('performs a series of undo and redo operations synchronously', function() { + this.doc.create({ test: 5 }), + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }), + expect(this.doc.data).to.eql({ test: 7 }); + this.doc.undo(), + expect(this.doc.data).to.eql({ test: 5 }); + this.doc.redo(), + expect(this.doc.data).to.eql({ test: 7 }); + this.doc.undo(), + expect(this.doc.data).to.eql({ test: 5 }); + this.doc.redo(), + expect(this.doc.data).to.eql({ test: 7 }); + this.doc.undo(), + expect(this.doc.data).to.eql({ test: 5 }); + this.doc.redo(), + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('undoes one of two operations', function(allDone) { + this.doc.undoComposeTimeout = -1; + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(4); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('undoes two of two operations', function(allDone) { + this.doc.undoComposeTimeout = -1; + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + this.doc.undo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(5); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('reoes one of two operations', function(allDone) { + this.doc.undoComposeTimeout = -1; + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(6); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('reoes two of two operations', function(allDone) { + this.doc.undoComposeTimeout = -1; + async.series([ + this.doc.create.bind(this.doc, { test: 5 }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), + this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), + this.doc.undo.bind(this.doc), + this.doc.undo.bind(this.doc), + this.doc.redo.bind(this.doc), + this.doc.redo.bind(this.doc), + function(done) { + expect(this.doc.version).to.equal(7); + expect(this.doc.data).to.eql({ test: 10 }); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this) + ], allDone); + }); + + it('calls undo, when canUndo is false', function(done) { + expect(this.doc.canUndo()).to.equal(false); + this.doc.undo(done); + }); + + it('calls undo, when canUndo is false - no callback', function() { + expect(this.doc.canUndo()).to.equal(false); + this.doc.undo(); + }); + + it('calls redo, when canRedo is false', function(done) { + expect(this.doc.canRedo()).to.equal(false); + this.doc.redo(done); + }); + + it('calls redo, when canRedo is false - no callback', function() { + expect(this.doc.canRedo()).to.equal(false); + this.doc.redo(); + }); + + it('preserves source on create', function(done) { + this.doc.on('create', function(source) { + expect(source).to.equal('test source'); + done(); + }); + this.doc.create({ test: 5 }, null, { source: 'test source' }); + }); + + it('preserves source on del', function(done) { + this.doc.on('del', function(oldContent, source) { + expect(source).to.equal('test source'); + done(); + }); + this.doc.create({ test: 5 }); + this.doc.del({ source: 'test source' }); + }); + + it('preserves source on submitOp', function(done) { + this.doc.on('op', function(op, source) { + expect(source).to.equal('test source'); + done(); + }); + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { source: 'test source' }); + }); + + it('preserves source on undo', function(done) { + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.on('op', function(op, source) { + expect(source).to.equal('test source'); + done(); + }); + this.doc.undo({ source: 'test source' }); + }); + + it('preserves source on redo', function(done) { + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.undo(); + this.doc.on('op', function(op, source) { + expect(source).to.equal('test source'); + done(); + }); + this.doc.redo({ source: 'test source' }); + }); + + it('has source=false on remote operations', function(done) { + this.doc.on('op', function(op, source) { + expect(source).to.equal(false); + done(); + }); + this.doc.subscribe(function() { + this.doc2.preventCompose = true; + this.doc2.create({ test: 5 }); + this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ]); + }.bind(this)); + }); + + it('composes undoable operations within time limit', function(done) { + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + setTimeout(function() { + this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); + expect(this.doc.data).to.eql({ test: 10 }); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + done(); + }.bind(this), 2); + }); + + it('composes undoable operations correctly', function() { + this.doc.create({ a: 1, b: 2 }); + this.doc.submitOp([ { p: [ 'a' ], od: 1 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'b' ], od: 2 } ], { undoable: true }); + expect(this.doc.data).to.eql({}); + expect(this.doc.canRedo()).to.equal(false); + var opCalled = false; + this.doc.once('op', function(op) { + opCalled = true; + expect(op).to.eql([ { p: [ 'b' ], oi: 2 }, { p: [ 'a' ], oi: 1 } ]); + }); + this.doc.undo(); + expect(opCalled).to.equal(true); + expect(this.doc.data).to.eql({ a: 1, b: 2 }); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql({}); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('does not compose undoable operations outside time limit', function(done) { + this.doc.undoComposeTimeout = 1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + setTimeout(function () { + this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); + expect(this.doc.data).to.eql({ test: 10 }); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + done(); + }.bind(this), 3); + }); + + it('does not compose undoable operations, if undoComposeTimeout < 0', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); + expect(this.doc.data).to.eql({ test: 10 }); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + }); + + it('does not compose undoable operations, if type does not support compose nor composeSimilar', function() { + this.doc.create(5, invertibleType.type.uri); + this.doc.submitOp(2, { undoable: true }); + expect(this.doc.data).to.equal(7); + this.doc.submitOp(2, { undoable: true }); + expect(this.doc.data).to.equal(9); + this.doc.undo(); + expect(this.doc.data).to.equal(7); + this.doc.undo(); + expect(this.doc.data).to.equal(5); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.equal(7); + this.doc.redo(); + expect(this.doc.data).to.equal(9); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('uses applyAndInvert, if available', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('two') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); + this.doc.submitOp([ otRichText.Action.createInsertText('one') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); + }); + + it('does not add an undo level, if type is not invertible', function() { + this.doc.create('two', otText.type.uri); + this.doc.submitOp([ 'one' ], { undoable: true }); + expect(this.doc.data).to.eql('onetwo'); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql('onetwo'); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('composes similar operations', function() { + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ + otRichText.Action.createInsertText('one') + ], { undoable: true }); + this.doc.submitOp([ + otRichText.Action.createRetain(3), + otRichText.Action.createInsertText('two') + ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); + expect(this.doc.canRedo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql([]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('does not compose dissimilar operations', function() { + this.doc.create([ + otRichText.Action.createInsertText(' ') + ], otRichText.type.uri); + + this.doc.submitOp([ + otRichText.Action.createRetain(1), + otRichText.Action.createInsertText('two') + ], { undoable: true }); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText(' two') + ]); + + this.doc.submitOp([ + otRichText.Action.createInsertText('one') + ], { undoable: true }); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText('one two') + ]); + + this.doc.undo(); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText(' two') + ]); + + this.doc.undo(); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText(' ') + ]); + + this.doc.redo(); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText(' two') + ]); + + this.doc.redo(); + expect(this.doc.data).to.eql([ + otRichText.Action.createInsertText('one two') + ]); + }); + + it('does not add no-ops to the undo stack on undoable operation', function() { + var opCalled = false; + this.doc.create([ otRichText.Action.createInsertText('test', [ 'key', 'value' ]) ], otRichText.type.uri); + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createRetain(4, [ 'key', 'value' ]) ]); + opCalled = true; + }); + this.doc.submitOp([ otRichText.Action.createRetain(4, [ 'key', 'value' ]) ], { undoable: true }); + expect(opCalled).to.equal(true); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test', [ 'key', 'value' ]) ]); + expect(this.doc.canUndo()).to.eql(false); + expect(this.doc.canRedo()).to.eql(false); + }); + + it('limits the size of the undo stack', function() { + this.doc.undoLimit = 2; + this.doc.undoComposeTimeout = -1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + expect(this.doc.data).to.eql({ test: 11 }); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.canUndo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 7 }); + }); + + it('limits the size of the undo stack, after adjusting the limit', function() { + this.doc.undoLimit = 100; + this.doc.undoComposeTimeout = -1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.undoLimit = 2; + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + expect(this.doc.data).to.eql({ test: 15 }); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.canUndo()).to.equal(true); + this.doc.undo(); + expect(this.doc.canUndo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 11 }); + }); + + it('does not limit the size of the stacks on undo and redo operations', function() { + this.doc.undoLimit = 100; + this.doc.undoComposeTimeout = -1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.undoLimit = 2; + expect(this.doc.data).to.eql({ test: 15 }); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + this.doc.redo(); + this.doc.redo(); + this.doc.redo(); + this.doc.redo(); + this.doc.redo(); + expect(this.doc.data).to.eql({ test: 15 }); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + }); + + it('does not compose the next operation after undo', function() { + this.doc.create({ test: 5 }); + this.doc.undoComposeTimeout = -1; + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.undoComposeTimeout = 1000; + this.doc.undo(); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // composed + expect(this.doc.data).to.eql({ test: 11 }); + expect(this.doc.canUndo()).to.equal(true); + + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + }); + + it('does not compose the next operation after undo and redo', function() { + this.doc.create({ test: 5 }); + this.doc.undoComposeTimeout = -1; + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.undoComposeTimeout = 1000; + this.doc.undo(); + this.doc.redo(); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // composed + expect(this.doc.data).to.eql({ test: 13 }); + expect(this.doc.canUndo()).to.equal(true); + + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 9 }); + expect(this.doc.canUndo()).to.equal(true); + + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 7 }); + expect(this.doc.canUndo()).to.equal(true); + + this.doc.undo(); + expect(this.doc.data).to.eql({ test: 5 }); + expect(this.doc.canUndo()).to.equal(false); + }); + + it('clears stacks on del', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.undo(); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(true); + this.doc.del(); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('transforms the stacks by remote operations', function(done) { + this.doc2.subscribe(); + this.doc.subscribe(); + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + setTimeout(function() { + this.doc.once('op', function(op, source) { + expect(source).to.equal(false); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); + done(); + }.bind(this)); + this.doc2.submitOp([ otRichText.Action.createInsertText('ABC') ]); + }.bind(this)); + }); + + it('transforms the stacks by remote operations and removes no-ops', function(done) { + this.doc2.subscribe(); + this.doc.subscribe(); + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + setTimeout(function() { + this.doc.once('op', function(op, source) { + expect(source).to.equal(false); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('24') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('124') ]); + expect(this.doc.canRedo()).to.equal(false); + done(); + }.bind(this)); + this.doc2.submitOp([ otRichText.Action.createDelete(1) ]); + }.bind(this)); + }); + + it('transforms the stacks by a local FIXED operation', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + this.doc.submitOp([ otRichText.Action.createInsertText('ABC') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); + }); + + it('transforms the stacks by a local FIXED operation and removes no-ops', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + this.doc.submitOp([ otRichText.Action.createDelete(1) ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('24') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('124') ]); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('transforms the stacks using transform', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create(0, invertibleType.type.uri); + this.doc.submitOp(1, { undoable: true }); + this.doc.submitOp(10, { undoable: true }); + this.doc.submitOp(100, { undoable: true }); + this.doc.submitOp(1000, { undoable: true }); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.equal(11); + this.doc.submitOp(10000); + this.doc.undo(); + expect(this.doc.data).to.equal(10001); + this.doc.undo(); + expect(this.doc.data).to.equal(10000); + this.doc.redo(); + expect(this.doc.data).to.equal(10001); + this.doc.redo(); + expect(this.doc.data).to.equal(10011); + this.doc.redo(); + expect(this.doc.data).to.equal(10111); + this.doc.redo(); + expect(this.doc.data).to.equal(11111); + }); + + it('transforms the stacks using transformX', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create(0, invertibleType.typeWithTransformX.uri); + this.doc.submitOp(1, { undoable: true }); + this.doc.submitOp(10, { undoable: true }); + this.doc.submitOp(100, { undoable: true }); + this.doc.submitOp(1000, { undoable: true }); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.equal(11); + this.doc.submitOp(10000); + this.doc.undo(); + expect(this.doc.data).to.equal(10001); + this.doc.undo(); + expect(this.doc.data).to.equal(10000); + this.doc.redo(); + expect(this.doc.data).to.equal(10001); + this.doc.redo(); + expect(this.doc.data).to.equal(10011); + this.doc.redo(); + expect(this.doc.data).to.equal(10111); + this.doc.redo(); + expect(this.doc.data).to.equal(11111); + }); + + it('skips processing when submitting a no-op (no callback)', function(done) { + this.doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitOp([]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + + it('skips processing when submitting a no-op (with callback)', function(done) { + this.doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitOp([], done); + }); + + it('skips processing when submitting an identical snapshot (no callback)', function(done) { + this.doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + + it('skips processing when submitting an identical snapshot (with callback)', function(done) { + this.doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], done); + }); + + describe('operationType', function() { + it('reports UNDOABLE operationType', function(done) { + this.doc.create({ test: 5 }); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('UNDOABLE'); + done(); + }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + }); + + it('reports UNDO operationType', function(done) { + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('UNDO'); + done(); + }); + this.doc.undo(); + }); + + it('reports REDO operationType', function(done) { + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.undo(); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('REDO'); + done(); + }); + this.doc.redo(); + }); + + it('reports FIXED operationType (local operation, undoable=false)', function(done) { + this.doc.create({ test: 5 }); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('FIXED'); + done(); + }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ]); + }); + + it('reports FIXED operationType (local operation, undoable=true but type is not invertible)', function(done) { + this.doc.create('', otText.type.uri); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('FIXED'); + done(); + }); + this.doc.submitOp([ 'test' ], { undoable: true }); + }); + + it('reports FIXED operationType (remote operation, undoable=false)', function(done) { + this.doc.subscribe(); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(false); + expect(operationType).to.equal('FIXED'); + done(); + }); + this.doc2.preventCompose = true; + this.doc2.create({ test: 5 }); + this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ]); + }); + + it('reports FIXED operationType (remote operation, undoable=true)', function(done) { + this.doc.subscribe(); + this.doc.on('op', function(op, source, operationType) { + expect(source).to.equal(false); + expect(operationType).to.equal('FIXED'); + done(); + }); + this.doc2.preventCompose = true; + this.doc2.create({ test: 5 }); + this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + }); + }); + + describe('fixup operations', function() { + describe('basic tests', function() { + beforeEach(function() { + this.assert = function(text) { + var expected = text ? [ otRichText.Action.createInsertText(text) ] : []; + expect(this.doc.data).to.eql(expected); + return this; + }; + this.submitOp = function(op, options) { + this.doc.submitOp([ otRichText.Action.createInsertText(op) ], options); + return this; + }; + this.submitSnapshot = function(snapshot, options) { + this.doc.submitSnapshot([ otRichText.Action.createInsertText(snapshot) ], options); + return this; + }; + this.undo = function() { + this.doc.undo(); + return this; + }; + this.redo = function() { + this.doc.redo(); + return this; + }; + + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.submitOp('d', { undoable: true }).assert('d'); + this.submitOp('c', { undoable: true }).assert('cd'); + this.submitOp('b', { undoable: true }).assert('bcd'); + this.submitOp('a', { undoable: true }).assert('abcd'); + this.undo().assert('bcd'); + this.undo().assert('cd'); + expect(this.doc.canUndo()).to.equal(true); + expect(this.doc.canRedo()).to.equal(true); + }); + + it('submits an operation (transforms undo stack, transforms redo stack)', function() { + this.submitOp('!').assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); + }); + + it('submits an operation (fixes up undo stack, transforms redo stack)', function() { + this.submitOp('!', { fixUpUndoStack: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); + }); + + it('submits an operation (transforms undo stack, fixes up redo stack)', function() { + this.submitOp('!', { fixUpRedoStack: true }).assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); + + it('submits an operation (fixes up undo stack, fixes up redo stack)', function() { + this.submitOp('!', { fixUpUndoStack: true, fixUpRedoStack: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); + + it('submits a snapshot (transforms undo stack, transforms redo stack)', function() { + this.submitSnapshot('!cd').assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); + }); + + it('submits a snapshot (fixes up undo stack, transforms redo stack)', function() { + this.submitSnapshot('!cd', { fixUpUndoStack: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); + }); + + it('submits a snapshot (transforms undo stack, fixes up redo stack)', function() { + this.submitSnapshot('!cd', { fixUpRedoStack: true }).assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); + + it('submits a snapshot (fixes up undo stack, fixes up redo stack)', function() { + this.submitSnapshot('!cd', { fixUpUndoStack: true, fixUpRedoStack: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); + }); + + describe('no-ops', function() { + it('removes a no-op from the undo stack', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('d') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('c') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('b') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createInsertText('a') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('cd') ]); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { fixUpUndoStack: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('bd') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abd') ]); + expect(this.doc.canRedo()).to.equal(false); + }); + + it('removes a no-op from the redo stack', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([ otRichText.Action.createInsertText('abcd') ], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('cd') ]); + this.doc.submitOp([ otRichText.Action.createDelete(1) ], { fixUpRedoStack: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([]); + expect(this.doc.canRedo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('bd') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abd') ]); + expect(this.doc.canUndo()).to.equal(false); + }); + }); + }); + + describe('submitSnapshot', function() { + describe('basic tests', function() { + it('submits a snapshot when document is not created (no callback, no options)', function(done) { + this.doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + this.doc.submitSnapshot(7); + }); + + it('submits a snapshot when document is not created (no callback, with options)', function(done) { + this.doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + this.doc.submitSnapshot(7, { source: 'test' }); + }); + + it('submits a snapshot when document is not created (with callback, no options)', function(done) { + this.doc.on('error', done); + this.doc.submitSnapshot(7, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + }); + + it('submits a snapshot when document is not created (with callback, with options)', function(done) { + this.doc.on('error', done); + this.doc.submitSnapshot(7, { source: 'test' }, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + }); + + it('submits a snapshot with source (no callback)', function(done) { + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + expect(source).to.equal('test'); + done(); + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }); + }); + + it('submits a snapshot with source (with callback)', function(done) { + var opEmitted = false; + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal('test'); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + opEmitted = true; + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }, function(error) { + expect(opEmitted).to.equal(true); + done(error); + }); + }); + + it('submits a snapshot without source (no callback)', function(done) { + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + expect(source).to.equal(true); + done(); + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); + }); + + it('submits a snapshot without source (with callback)', function(done) { + var opEmitted = false; + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal(true); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + opEmitted = true; + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], function(error) { + expect(opEmitted).to.equal(true); + done(error); + }); + }); + + it('submits snapshots and supports undo and redo', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + expect(this.doc.canRedo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(this.doc.canUndo()).to.equal(false); + }); + + it('submits snapshots and composes operations', function() { + this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(this.doc.canUndo()).to.equal(false); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + expect(this.doc.canRedo()).to.equal(false); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(this.doc.canUndo()).to.equal(false); + }); + + it('submits a snapshot and syncs it', function(done) { + this.doc2.on('create', function() { + this.doc2.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); + }.bind(this)); + this.doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(source).to.equal(false); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + done(); + }.bind(this)); + this.doc2.subscribe(); + this.doc.subscribe(); + this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + }); + + it('submits undoable and fixed operations', function() { + this.doc.undoComposeTimeout = -1; + this.doc.create([], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('a') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('ab') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcd') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcde') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { undoable: true }); + this.doc.undo(); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcd') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc123d') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); + this.doc.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('123') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123de') ]); + this.doc.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123def') ]); + }); + + it('submits a snapshot without a diffHint', function() { + var opCalled = 0; + this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); + expect(op).to.eql([ otRichText.Action.createDelete(1) ]); + opCalled++; + }.bind(this)); + this.doc.undo(); + + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createInsertText('a') ]); + opCalled++; + }.bind(this)); + this.doc.redo(); + + expect(opCalled).to.equal(2); + }); + + it('submits a snapshot with a diffHint', function() { + var opCalled = 0; + this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true, diffHint: 2 }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); + expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createDelete(1) ]); + opCalled++; + }.bind(this)); + this.doc.undo(); + + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createInsertText('a') ]); + opCalled++; + }.bind(this)); + this.doc.redo(); + + expect(opCalled).to.equal(2); + }); + }); + + describe('no diff nor diffX', function() { + it('submits a snapshot (no callback)', function(done) { + this.doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4024); + done(); + }); + this.doc.create(5, invertibleType.type.uri); + this.doc.submitSnapshot(7); + }); + + it('submits a snapshot (with callback)', function(done) { + this.doc.on('error', done); + this.doc.create(5, invertibleType.type.uri); + this.doc.submitSnapshot(7, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4024); + done(); + }); + }); + }); + + describe('with diff', function () { + it('submits a snapshot (non-undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiff.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + }); + it('submits a snapshot (undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiff.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + this.doc.undo(); + expect(this.doc.data).to.equal(5); + this.doc.redo(); + expect(this.doc.data).to.equal(7); + }); + }); + + describe('with diffX', function () { + it('submits a snapshot (non-undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiffX.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + }); + it('submits a snapshot (undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiffX.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + this.doc.undo(); + expect(this.doc.data).to.equal(5); + this.doc.redo(); + expect(this.doc.data).to.equal(7); + }); + }); + + describe('with diff and diffX', function () { + it('submits a snapshot (non-undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(this.doc.canUndo()).to.equal(false); + expect(this.doc.canRedo()).to.equal(false); + }); + it('submits a snapshot (undoable)', function() { + this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + this.doc.undo(); + expect(this.doc.data).to.equal(5); + this.doc.redo(); + expect(this.doc.data).to.equal(7); + }); + }); + }); +}); From 15cdd1d3d451a2b5dcbdadd9fb551c912975738d Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 12:24:25 +0200 Subject: [PATCH 05/27] Update tested nodejs versions --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5c87e1e6d..21efafe46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - "10" - - "9" - "8" - "6" script: "npm run jshint && npm run test-cover" From cfca37ff92ecfe0cfba0de985e3651d4f3a51ec9 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 12:38:21 +0200 Subject: [PATCH 06/27] Make destroy wait for unsubscribe --- lib/client/doc.js | 11 ++++++++--- test/client/subscribe.js | 13 ++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 33621cb9c..796df6bd7 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -105,10 +105,15 @@ Doc.prototype.destroy = function(callback) { var doc = this; doc.whenNothingPending(function() { if (doc.wantSubscribe) { - doc.unsubscribe(); + doc.unsubscribe(function(err) { + if (!err) doc.connection._destroyDoc(doc); + if (callback) return callback(err); + if (err) this.emit('error', err); + }); + } else { + doc.connection._destroyDoc(doc); + if (callback) callback(); } - doc.connection._destroyDoc(doc); - if (callback) callback(); }); }; diff --git a/test/client/subscribe.js b/test/client/subscribe.js index db2bea1b2..b24a94749 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -414,7 +414,7 @@ describe('client subscribe', function() { doc2.subscribe(function(err) { if (err) return done(err); doc2.on('op', function(op, context) { - done(); + done(new Error('Should not get op event')); }); doc2.destroy(function(err) { if (err) return done(err); @@ -425,6 +425,17 @@ describe('client subscribe', function() { }); }); + it('doc destroy removes doc from connection when doc is not subscribed', function(done) { + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + expect(connection.getExisting('dogs', 'fido')).equal(doc); + doc.destroy(function(err) { + if (err) return done(err); + expect(connection.getExisting('dogs', 'fido')).equal(undefined); + done(); + }); + }); + it('bulk unsubscribe stops op updates', function(done) { var connection = this.backend.connect(); var connection2 = this.backend.connect(); From 5e009d17d0b6b665eb25abc0d538d1bc67e5dc6b Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 12:52:59 +0200 Subject: [PATCH 07/27] Simplify the code --- lib/client/doc.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 796df6bd7..d75e83085 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -106,9 +106,13 @@ Doc.prototype.destroy = function(callback) { doc.whenNothingPending(function() { if (doc.wantSubscribe) { doc.unsubscribe(function(err) { - if (!err) doc.connection._destroyDoc(doc); - if (callback) return callback(err); - if (err) this.emit('error', err); + if (err) { + if (callback) callback(err); + else this.emit('error', err); + return; + } + doc.connection._destroyDoc(doc); + if (callback) callback(); }); } else { doc.connection._destroyDoc(doc); From efd6e6ee9f08f2a337f4e6bced5a5198504a4be2 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 16 Jul 2018 16:58:19 +0200 Subject: [PATCH 08/27] Support skipNoop option --- README.md | 2 ++ lib/client/doc.js | 10 +++++++--- test/client/undo-redo.js | 26 ++++++++++++++++++++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2023a0908..48df450c8 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,7 @@ Apply operation to document and send it to the server. [operations for the default `'ot-json0'` type](https://github.com/ottypes/json0#summary-of-operations). Call this after you've either fetched or subscribed to the document. * `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. +* `options.skipNoop` Should processing be skipped entirely, if `op` is a no-op. Defaults to `false`. * `options.undoable` Should it be possible to undo this operation, default=false. * `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. * `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. @@ -288,6 +289,7 @@ Diff the current and the provided snapshots to generate an operation, apply the `snapshot` structure depends on the document type. Call this after you've either fetched or subscribed to the document. * `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. +* `options.skipNoop` Should processing be skipped entirely, if the generated operation is a no-op. Defaults to `false`. * `options.undoable` Should it be possible to undo this operation, default=false. * `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. * `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. diff --git a/lib/client/doc.js b/lib/client/doc.js index c6b9fa5bb..117d1cb12 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -809,7 +809,7 @@ Doc.prototype._sendOp = function() { // @param [op.op] // @param [op.del] // @param [op.create] -// @param options { source, operationType, undoOp, fixUpUndoStack, fixUpRedoStack } +// @param options { source, skipNoop, operationType, undoOp, fixUpUndoStack, fixUpRedoStack } // @param [callback] called when operation is submitted Doc.prototype._submit = function(op, options, callback) { if (!options) options = {}; @@ -827,8 +827,8 @@ Doc.prototype._submit = function(op, options, callback) { } // Try to normalize the op. This removes trailing skip:0's and things like that. if (this.type.normalize) op.op = this.type.normalize(op.op); - // Try to skip processing empty operations. - if (this.type.isNoop && this.type.isNoop(op.op)) { + // Try to skip processing no-ops. + if (options.skipNoop && this.type.isNoop && this.type.isNoop(op.op)) { if (callback) process.nextTick(callback); return; } @@ -923,6 +923,7 @@ Doc.prototype._tryCompose = function(op) { // // @param component operation handled by the OT type // @param options.source passed into 'op' event handler +// @param options.skipNoop should processing be skipped entirely, if `component` is a no-op. // @param options.undoable should the operation be undoable // @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. // If false (default), op transforms undoStack. @@ -941,6 +942,7 @@ Doc.prototype.submitOp = function(component, options, callback) { var op = {op: component}; var submitOptions = { source: options && options.source, + skipNoop: options && options.skipNoop, operationType: options && options.undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, fixUpUndoStack: options && options.fixUpUndoStack, fixUpRedoStack: options && options.fixUpRedoStack @@ -956,6 +958,7 @@ Doc.prototype.submitOp = function(component, options, callback) { // // @param snapshot new snapshot data // @param options.source passed into 'op' event handler +// @param options.skipNoop should processing be skipped entirely, if the generated operation is a no-op. // @param options.undoable should the operation be undoable // @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. // If false (default), op transforms undoStack. @@ -1001,6 +1004,7 @@ Doc.prototype.submitSnapshot = function(snapshot, options, callback) { var submitOptions = { source: options && options.source, + skipNoop: options && options.skipNoop, operationType: undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, undoOp: undoOp, fixUpUndoStack: fixUpUndoStack, diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 6ba6a9866..d45821ede 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -773,12 +773,30 @@ describe('client undo/redo', function() { expect(this.doc.data).to.equal(11111); }); + it('does not skip processing when submitting a no-op by default', function(done) { + this.doc.on('op', function() { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitOp([]); + }); + + it('does not skip processing when submitting an identical snapshot by default', function(done) { + this.doc.on('op', function() { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }.bind(this)); + this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ]); + }); + it('skips processing when submitting a no-op (no callback)', function(done) { this.doc.on('op', function() { done(new Error('Should not emit `op`')); }); this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitOp([]); + this.doc.submitOp([], { skipNoop: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); done(); }); @@ -788,7 +806,7 @@ describe('client undo/redo', function() { done(new Error('Should not emit `op`')); }); this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitOp([], done); + this.doc.submitOp([], { skipNoop: true }, done); }); it('skips processing when submitting an identical snapshot (no callback)', function(done) { @@ -796,7 +814,7 @@ describe('client undo/redo', function() { done(new Error('Should not emit `op`')); }); this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); done(); }); @@ -806,7 +824,7 @@ describe('client undo/redo', function() { done(new Error('Should not emit `op`')); }); this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], done); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }, done); }); describe('operationType', function() { From d5f02251cd154c884a134e36a0c52f6e896e54b1 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 17 Jul 2018 12:26:03 +0200 Subject: [PATCH 09/27] Fails fast if type in not invertible --- README.md | 5 +- lib/client/doc.js | 39 +++++++------- test/client/undo-redo.js | 108 +++++++++++++++++++++++++++++++++------ 3 files changed, 115 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 48df450c8..a61f38968 100644 --- a/README.md +++ b/README.md @@ -251,8 +251,8 @@ 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'), 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('before op'), function(op, source, operationType) {...})` +An operation is about to be applied to the data. Params are the same as for the `op` event below. `doc.on('op', function(op, source, operationType) {...})` 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. `operationType` is one of the following: `"UNDOABLE"` _(local operation that can be undone)_, `"FIXED"` _(local or remote operation that can't be undone nor redone)_, `"UNDO"` _(local undo operation that can be redone)_ and `"REDO"` _(local redo operation that can be undone)_. @@ -398,6 +398,7 @@ Additional fields may be added to the error object for debugging context dependi * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type * 4024 - OT Type does not support `diff` nor `diffX` +* 4025 - OT Type does not support `invert` nor `applyAndInvert` ### 5000 - Internal error diff --git a/lib/client/doc.js b/lib/client/doc.js index 117d1cb12..e839ee4af 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -528,6 +528,10 @@ Doc.prototype._otApply = function(op, options) { var err = new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); return this.emit('error', err); } + var undoOp = options && options.undoOp || null; + var operationType = options && options.operationType || OPERATION_TYPES.FIXED; + var fixUpUndoStack = options && options.fixUpUndoStack || false; + var fixUpRedoStack = options && options.fixUpRedoStack || false; // Iteratively apply multi-component remote operations and rollback ops // (source === false) for the default JSON0 OT type. It could use @@ -558,9 +562,9 @@ Doc.prototype._otApply = function(op, options) { if (transformErr) return this._hardRollback(transformErr); } // Apply the individual op component - this.emit('before op', componentOp.op, source); - this._applyOp(componentOp, options); - this.emit('op', componentOp.op, source, 'FIXED'); + this.emit('before op', componentOp.op, source, operationType); + this._applyOp(componentOp, undoOp, operationType, fixUpUndoStack, fixUpRedoStack); + this.emit('op', componentOp.op, source, operationType); } // Pop whatever was submitted since we started applying this op this._popApplyStack(stackLength); @@ -569,9 +573,9 @@ Doc.prototype._otApply = function(op, options) { // 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); + this.emit('before op', op.op, source, operationType); // Apply the operation to the local data, mutating it in place - var operationType = this._applyOp(op, options); + this._applyOp(op, undoOp, operationType, fixUpUndoStack, fixUpRedoStack); // Emit an '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. @@ -603,11 +607,7 @@ Doc.prototype._otApply = function(op, options) { }; // Applies `op` to `this.data` and updates the undo/redo stacks. -Doc.prototype._applyOp = function(op, options) { - var undoOp = options && options.undoOp || null; - var operationType = options && options.operationType || OPERATION_TYPES.FIXED; - var fixUpUndoStack = options && options.fixUpUndoStack; - var fixUpRedoStack = options && options.fixUpRedoStack; +Doc.prototype._applyOp = function(op, undoOp, operationType, fixUpUndoStack, fixUpRedoStack) { var needsUndoOp = operationType !== OPERATION_TYPES.FIXED || fixUpUndoStack || fixUpRedoStack; if (needsUndoOp && undoOp == null) { @@ -615,16 +615,10 @@ Doc.prototype._applyOp = function(op, options) { var result = this.type.applyAndInvert(this.data, op.op); this.data = result[0]; undoOp = { op: result[1] }; - - } else if (this.type.invert) { - this.data = this.type.apply(this.data, op.op); - undoOp = { op: this.type.invert(op.op) }; - } else { this.data = this.type.apply(this.data, op.op); - operationType = OPERATION_TYPES.FIXED; + undoOp = { op: this.type.invert(op.op) }; } - } else { this.data = this.type.apply(this.data, op.op); } @@ -643,8 +637,6 @@ Doc.prototype._applyOp = function(op, options) { this._updateStacksFixed(op, undoOp, fixUpUndoStack, fixUpRedoStack); break; }; - - return operationType; }; Doc.prototype._clearUndoRedo = function() { @@ -825,6 +817,15 @@ Doc.prototype._submit = function(op, options, callback) { if (callback) return callback(err); return this.emit('error', err); } + var notFixedOperation = options.operationType !== OPERATION_TYPES.FIXED; + var fixUpUndoStack = options.fixUpUndoStack; + var fixUpRedoStack = options.fixUpRedoStack; + var needsInvert = notFixedOperation || fixUpUndoStack || fixUpRedoStack; + if (needsInvert && !this.type.invert && !this.type.applyAndInvert) { + var err = new ShareDBError(4025, 'Cannot submit op. OT type does not support invert not applyAndInvert. ' + this.collection + '.' + this.id); + if (callback) return callback(err); + return this.emit('error', err); + } // Try to normalize the op. This removes trailing skip:0's and things like that. if (this.type.normalize) op.op = this.type.normalize(op.op); // Try to skip processing no-ops. diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index d45821ede..b98a62a58 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -7,9 +7,11 @@ var expect = require('expect.js'); var types = require('../../lib/types'); var otText = require('ot-text'); var otRichText = require('@teamwork/ot-rich-text'); +var richText = require('rich-text'); var invertibleType = require('./invertible-type'); types.register(otText.type); +types.register(richText.type); types.register(otRichText.type); types.register(invertibleType.type); types.register(invertibleType.typeWithDiff); @@ -397,16 +399,58 @@ describe('client undo/redo', function() { expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); }); - it('does not add an undo level, if type is not invertible', function() { + it('fails to submit undoable op, if type is not invertible (callback)', function(done) { this.doc.create('two', otText.type.uri); + this.doc.on('error', done); + this.doc.submitOp([ 'one' ], { undoable: true }, function(err) { + expect(err.code).to.equal(4025); + done(); + }); + }); + + it('fails to submit undoable op, if type is not invertible (no callback)', function(done) { + this.doc.create('two', otText.type.uri); + this.doc.on('error', function(err) { + expect(err.code).to.equal(4025); + done(); + }); this.doc.submitOp([ 'one' ], { undoable: true }); - expect(this.doc.data).to.eql('onetwo'); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); - this.doc.undo(); - expect(this.doc.data).to.eql('onetwo'); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + }); + + it('fails to submit undoable snapshot, if type is not invertible (callback)', function(done) { + this.doc.create([], richText.type.uri); + this.doc.on('error', done); + this.doc.submitSnapshot([ { insert: 'abc' } ], { undoable: true }, function(err) { + expect(err.code).to.equal(4025); + done(); + }); + }); + + it('fails to submit undoable snapshot, if type is not invertible (no callback)', function(done) { + this.doc.create([], richText.type.uri); + this.doc.on('error', function(err) { + expect(err.code).to.equal(4025); + done(); + }); + this.doc.submitSnapshot([ { insert: 'abc' } ], { undoable: true }); + }); + + it('fails to submit with fixUpUndoStack, if type is not invertible', function(done) { + this.doc.create('two', otText.type.uri); + this.doc.on('error', done); + this.doc.submitOp([ 'one' ], { fixUpUndoStack: true }, function(err) { + expect(err.code).to.equal(4025); + done(); + }); + }); + + it('fails to submit with fixUpRedoStack, if type is not invertible', function(done) { + this.doc.create('two', otText.type.uri); + this.doc.on('error', done); + this.doc.submitOp([ 'one' ], { fixUpRedoStack: true }, function(err) { + expect(err.code).to.equal(4025); + done(); + }); }); it('composes similar operations', function() { @@ -829,8 +873,15 @@ describe('client undo/redo', function() { describe('operationType', function() { it('reports UNDOABLE operationType', function(done) { + var beforeOpCalled = false; this.doc.create({ test: 5 }); + this.doc.on('before op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('UNDOABLE'); + beforeOpCalled = true; + }); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(true); expect(operationType).to.equal('UNDOABLE'); done(); @@ -839,9 +890,16 @@ describe('client undo/redo', function() { }); it('reports UNDO operationType', function(done) { + var beforeOpCalled = false; this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.doc.on('before op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('UNDO'); + beforeOpCalled = true; + }); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(true); expect(operationType).to.equal('UNDO'); done(); @@ -850,10 +908,17 @@ describe('client undo/redo', function() { }); it('reports REDO operationType', function(done) { + var beforeOpCalled = false; this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.undo(); + this.doc.on('before op', function(op, source, operationType) { + expect(source).to.equal(true); + expect(operationType).to.equal('REDO'); + beforeOpCalled = true; + }); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(true); expect(operationType).to.equal('REDO'); done(); @@ -862,28 +927,32 @@ describe('client undo/redo', function() { }); it('reports FIXED operationType (local operation, undoable=false)', function(done) { + var beforeOpCalled = false; this.doc.create({ test: 5 }); - this.doc.on('op', function(op, source, operationType) { + this.doc.on('before op', function(op, source, operationType) { expect(source).to.equal(true); expect(operationType).to.equal('FIXED'); - done(); + beforeOpCalled = true; }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ]); - }); - - it('reports FIXED operationType (local operation, undoable=true but type is not invertible)', function(done) { - this.doc.create('', otText.type.uri); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(true); expect(operationType).to.equal('FIXED'); done(); }); - this.doc.submitOp([ 'test' ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], na: 2 } ]); }); it('reports FIXED operationType (remote operation, undoable=false)', function(done) { + var beforeOpCalled = false; this.doc.subscribe(); + this.doc.on('before op', function(op, source, operationType) { + expect(source).to.equal(false); + expect(operationType).to.equal('FIXED'); + beforeOpCalled = true; + }); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(false); expect(operationType).to.equal('FIXED'); done(); @@ -894,8 +963,15 @@ describe('client undo/redo', function() { }); it('reports FIXED operationType (remote operation, undoable=true)', function(done) { + var beforeOpCalled = false; this.doc.subscribe(); + this.doc.on('before op', function(op, source, operationType) { + expect(source).to.equal(false); + expect(operationType).to.equal('FIXED'); + beforeOpCalled = true; + }); this.doc.on('op', function(op, source, operationType) { + expect(beforeOpCalled).to.equal(true); expect(source).to.equal(false); expect(operationType).to.equal('FIXED'); done(); From ade98a4a507088a10b478ef88a38e36b771607ab Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 13:12:13 +0200 Subject: [PATCH 10/27] Implement UndoManager (WIP) --- lib/client/connection.js | 41 ++ lib/client/doc.js | 278 ++-------- lib/client/undoManager.js | 273 ++++++++++ package-lock.json | 164 ++---- package.json | 3 +- test/client/submit.js | 11 +- test/client/undo-redo.js | 1037 +++++++++++++++---------------------- 7 files changed, 817 insertions(+), 990 deletions(-) create mode 100644 lib/client/undoManager.js diff --git a/lib/client/connection.js b/lib/client/connection.js index f4cc298e6..97b8c00ba 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -1,5 +1,6 @@ var Doc = require('./doc'); var Query = require('./query'); +var UndoManager = require('./undoManager'); var emitter = require('../emitter'); var ShareDBError = require('../error'); var types = require('../types'); @@ -33,6 +34,9 @@ function Connection(socket) { // (created documents MUST BE UNIQUE) this.collections = {}; + // A list of active UndoManagers. + this.undoManagers = []; + // Each query is created with an id that the server uses when it sends us // info about the query (updates, etc) this.nextQueryId = 1; @@ -584,3 +588,40 @@ Connection.prototype._firstQuery = function(fn) { } } }; + +Connection.prototype.undoManager = function(options) { + var undoManager = new UndoManager(this, options); + this.undoManagers.push(undoManager); + return undoManager; +}; + +Connection.prototype.removeUndoManager = function(undoManager) { + var index = this.undoManagers.indexOf(undoManager); + if (index >= 0) { + this.undoManagers.splice(index, 1); + } +}; + +Connection.prototype.onDocLoad = function(doc) { + for (var i = 0; i < this.undoManagers.length; i++) { + this.undoManagers[i].onDocLoad(doc); + } +}; + +Connection.prototype.onDocCreate = function(doc) { + for (var i = 0; i < this.undoManagers.length; i++) { + this.undoManagers[i].onDocCreate(doc); + } +}; + +Connection.prototype.onDocDelete = function(doc) { + for (var i = 0; i < this.undoManagers.length; i++) { + this.undoManagers[i].onDocDelete(doc); + } +}; + +Connection.prototype.onDocOp = function(doc, op, undoOp, source, undoable, fixUp) { + for (var i = 0; i < this.undoManagers.length; i++) { + this.undoManagers[i].onDocOp(doc, op, undoOp, source, undoable, fixUp); + } +}; diff --git a/lib/client/doc.js b/lib/client/doc.js index e839ee4af..0915e9f8b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -44,13 +44,6 @@ var types = require('../types'); * - `load ()` Fired when a new snapshot is ingested from a fetch, subscribe, or query */ -var OPERATION_TYPES = { - UNDOABLE: 'UNDOABLE', // basic operation that can be undone - FIXED: 'FIXED', // basic operation that cannot be undone - UNDO: 'UNDO', // undo operation - REDO: 'REDO' // redo operation -}; - module.exports = Doc; function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -64,19 +57,6 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - // Undo stack for local operations. - this.undoStack = []; - // Redo stack for local operations. - this.redoStack = []; - // The max number of undo operations to keep on the stack. - this.undoLimit = 100; - // The max time difference between operations in milliseconds, - // which still allows the operations to be composed on the undoStack. - this.undoComposeTimeout = 1000; - // The timestamp of the previous reversible operation. Used to determine if - // the next reversible operation can be composed on the undoStack. - this.previousUndoableOperationTime = -Infinity; - // Array of callbacks or nulls as placeholders this.inflightFetch = []; this.inflightSubscribe = []; @@ -211,7 +191,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.data = (this.type && this.type.deserialize) ? this.type.deserialize(snapshot.data) : snapshot.data; - this._clearUndoRedo(); + this.connection.onDocLoad(this); this.emit('load'); callback && callback(); }; @@ -529,9 +509,8 @@ Doc.prototype._otApply = function(op, options) { return this.emit('error', err); } var undoOp = options && options.undoOp || null; - var operationType = options && options.operationType || OPERATION_TYPES.FIXED; - var fixUpUndoStack = options && options.fixUpUndoStack || false; - var fixUpRedoStack = options && options.fixUpRedoStack || false; + var undoable = options && options.undoable || false; + var fixUp = options && options.fixUp || false; // Iteratively apply multi-component remote operations and rollback ops // (source === false) for the default JSON0 OT type. It could use @@ -562,9 +541,9 @@ Doc.prototype._otApply = function(op, options) { if (transformErr) return this._hardRollback(transformErr); } // Apply the individual op component - this.emit('before op', componentOp.op, source, operationType); - this._applyOp(componentOp, undoOp, operationType, fixUpUndoStack, fixUpRedoStack); - this.emit('op', componentOp.op, source, operationType); + this.emit('before op', componentOp.op, source); + this._applyOp(componentOp, undoOp, source, undoable, fixUp); + this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op this._popApplyStack(stackLength); @@ -573,15 +552,15 @@ Doc.prototype._otApply = function(op, options) { // 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, operationType); + this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place - this._applyOp(op, undoOp, operationType, fixUpUndoStack, fixUpRedoStack); + this._applyOp(op, undoOp, source, undoable, fixUp); // Emit an '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, operationType); + this.emit('op', op.op, source); return; } @@ -592,7 +571,7 @@ Doc.prototype._otApply = function(op, options) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - this._clearUndoRedo(); + this.connection.onDocCreate(this); this.emit('create', source); return; } @@ -600,17 +579,15 @@ Doc.prototype._otApply = function(op, options) { if (op.del) { var oldData = this.data; this._setType(null); - this._clearUndoRedo(); + this.connection.onDocDelete(this); this.emit('del', oldData, source); return; } }; // Applies `op` to `this.data` and updates the undo/redo stacks. -Doc.prototype._applyOp = function(op, undoOp, operationType, fixUpUndoStack, fixUpRedoStack) { - var needsUndoOp = operationType !== OPERATION_TYPES.FIXED || fixUpUndoStack || fixUpRedoStack; - - if (needsUndoOp && undoOp == null) { +Doc.prototype._applyOp = function(op, undoOp, source, undoable, fixUp) { + if (undoOp == null && (undoable || fixUp)) { if (this.type.applyAndInvert) { var result = this.type.applyAndInvert(this.data, op.op); this.data = result[0]; @@ -623,131 +600,7 @@ Doc.prototype._applyOp = function(op, undoOp, operationType, fixUpUndoStack, fix this.data = this.type.apply(this.data, op.op); } - switch (operationType) { - case OPERATION_TYPES.UNDOABLE: - this._updateStacksUndoable(op, undoOp); - break; - case OPERATION_TYPES.UNDO: - this._updateStacksUndo(op, undoOp); - break; - case OPERATION_TYPES.REDO: - this._updateStacksRedo(op, undoOp); - break; - default: - this._updateStacksFixed(op, undoOp, fixUpUndoStack, fixUpRedoStack); - break; - }; -}; - -Doc.prototype._clearUndoRedo = function() { - this.undoStack.length = 0; - this.redoStack.length = 0; - this.previousUndoableOperationTime = -Infinity; -}; - -Doc.prototype._updateStacksUndoable = function(op, undoOp) { - var now = Date.now(); - - if (this.undoStack.length === 0 || now - this.previousUndoableOperationTime > this.undoComposeTimeout) { - this.undoStack.push(undoOp); - - } else if (this.type.composeSimilar) { - var lastOp = this.undoStack.pop(); - var composedOp = this.type.composeSimilar(undoOp.op, lastOp.op); - if (composedOp != null) { - this.undoStack.push({ op: composedOp }); - } else { - this.undoStack.push(lastOp, undoOp); - } - - } else if (this.type.compose) { - var lastOp = this.undoStack.pop(); - var composedOp = this.type.compose(undoOp.op, lastOp.op); - this.undoStack.push({ op: composedOp }); - - } else { - this.undoStack.push(undoOp); - } - - this.redoStack.length = 0; - this.previousUndoableOperationTime = now; - - var isNoop = this.type.isNoop; - if (isNoop && isNoop(this.undoStack[this.undoStack.length - 1].op)) { - this.undoStack.pop(); - } - - var itemsToRemove = this.undoStack.length - this.undoLimit; - if (itemsToRemove > 0) { - this.undoStack.splice(0, itemsToRemove); - } -}; - -Doc.prototype._updateStacksUndo = function(op, undoOp) { - if (!this.type.isNoop || !this.type.isNoop(undoOp.op)) { - this.redoStack.push(undoOp); - } - this.previousUndoableOperationTime = -Infinity; -}; - -Doc.prototype._updateStacksRedo = function(op, undoOp) { - if (!this.type.isNoop || !this.type.isNoop(undoOp.op)) { - this.undoStack.push(undoOp); - } - this.previousUndoableOperationTime = -Infinity; -}; - -Doc.prototype._updateStacksFixed = function(op, undoOp, fixUpUndoStack, fixUpRedoStack) { - if (fixUpUndoStack && this.undoStack.length > 0 && this.type.compose && undoOp) { - var lastOp = this.undoStack.pop(); - var composedOp = this.type.compose(undoOp.op, lastOp.op); - if (!this.type.isNoop || !this.type.isNoop(composedOp)) { - this.undoStack.push({ op: composedOp }); - } - } else { - this.undoStack = this._transformStack(this.undoStack, op.op); - } - - if (fixUpRedoStack && this.redoStack.length > 0 && this.type.compose && undoOp) { - var lastOp = this.redoStack.pop(); - var composedOp = this.type.compose(undoOp.op, lastOp.op); - if (!this.type.isNoop || !this.type.isNoop(composedOp)) { - this.redoStack.push({ op: composedOp }); - } - } else { - this.redoStack = this._transformStack(this.redoStack, op.op); - } -}; - -Doc.prototype._transformStack = function(stack, op) { - var transform = this.type.transform; - var transformX = this.type.transformX; - var isNoop = this.type.isNoop; - var newStack = []; - var newStackIndex = 0; - - for (var i = stack.length - 1; i >= 0; --i) { - var stackOp = stack[i].op; - var transformedStackOp; - var transformedOp; - - if (transformX) { - var result = transformX(op, stackOp); - transformedOp = result[0]; - transformedStackOp = result[1]; - } else { - transformedOp = transform(op, stackOp, 'left'); - transformedStackOp = transform(stackOp, op, 'right'); - } - - if (!isNoop || !isNoop(transformedStackOp)) { - newStack[newStackIndex++] = { op: transformedStackOp }; - } - - op = transformedOp; - } - - return newStack.reverse(); + this.connection.onDocOp(this, op, undoOp, source, undoable, fixUp); }; // ***** Sending operations @@ -801,11 +654,10 @@ Doc.prototype._sendOp = function() { // @param [op.op] // @param [op.del] // @param [op.create] -// @param options { source, skipNoop, operationType, undoOp, fixUpUndoStack, fixUpRedoStack } +// @param options { source, skipNoop, undoable, undoOp, fixUp } // @param [callback] called when operation is submitted Doc.prototype._submit = function(op, options, callback) { if (!options) options = {}; - if (!options.operationType) options.operationType = OPERATION_TYPES.FIXED; // Locally submitted ops must always have a truthy source if (!options.source) options.source = true; @@ -817,11 +669,10 @@ Doc.prototype._submit = function(op, options, callback) { if (callback) return callback(err); return this.emit('error', err); } - var notFixedOperation = options.operationType !== OPERATION_TYPES.FIXED; - var fixUpUndoStack = options.fixUpUndoStack; - var fixUpRedoStack = options.fixUpRedoStack; - var needsInvert = notFixedOperation || fixUpUndoStack || fixUpRedoStack; - if (needsInvert && !this.type.invert && !this.type.applyAndInvert) { + var undoable = options && options.undoable; + var fixUp = options && options.fixUp; + var needsUndoOp = undoable || fixUp; + if (needsUndoOp && !this.type.invert && !this.type.applyAndInvert) { var err = new ShareDBError(4025, 'Cannot submit op. OT type does not support invert not applyAndInvert. ' + this.collection + '.' + this.id); if (callback) return callback(err); return this.emit('error', err); @@ -926,12 +777,9 @@ Doc.prototype._tryCompose = function(op) { // @param options.source passed into 'op' event handler // @param options.skipNoop should processing be skipped entirely, if `component` is a no-op. // @param options.undoable should the operation be undoable -// @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. -// If false (default), op transforms undoStack. -// If true, op is inverted and composed into the last operation on the undoStack. -// @param options.fixUpRedoStack Determines how non-undoable op affects redoStack. -// If false (default), op transforms redoStack. -// If true, op is inverted and composed into the last operation on the redoStack. +// @param options.fixUp If true, this operation is meant to fix the current invalid state of the snapshot. +// It also updates UndoManagers in such a way that undo/redo will "skip" over the current state of the snapshot. +// This feature requires the OT type to implement `compose`. // @param [callback] called after operation submitted // // @fires before op, op @@ -944,9 +792,8 @@ Doc.prototype.submitOp = function(component, options, callback) { var submitOptions = { source: options && options.source, skipNoop: options && options.skipNoop, - operationType: options && options.undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, - fixUpUndoStack: options && options.fixUpUndoStack, - fixUpRedoStack: options && options.fixUpRedoStack + undoable: options && options.undoable, + fixUp: options && options.fixUp }; this._submit(op, submitOptions, callback); }; @@ -961,12 +808,9 @@ Doc.prototype.submitOp = function(component, options, callback) { // @param options.source passed into 'op' event handler // @param options.skipNoop should processing be skipped entirely, if the generated operation is a no-op. // @param options.undoable should the operation be undoable -// @param options.fixUpUndoStack Determines how non-undoable op affects undoStack. -// If false (default), op transforms undoStack. -// If true, op is inverted and composed into the last operation on the undoStack. -// @param options.fixUpRedoStack Determines how non-undoable op affects redoStack. -// If false (default), op transforms redoStack. -// If true, op is inverted and composed into the last operation on the redoStack. +// @param options.fixUp If true, this operation is meant to fix the current invalid state of the snapshot. +// It also updates UndoManagers in such a way that undo/redo will "skip" over the current state of the snapshot. +// This feature requires the OT type to implement `compose`. // @param options.diffHint a hint passed into diff/diffX // @param [callback] called after operation submitted @@ -988,10 +832,9 @@ Doc.prototype.submitSnapshot = function(snapshot, options, callback) { } var undoable = !!(options && options.undoable); - var fixUpUndoStack = options && options.fixUpUndoStack; - var fixUpRedoStack = options && options.fixUpRedoStack; + var fixUp = options && options.fixUp; var diffHint = options && options.diffHint; - var needsUndoOp = undoable || fixUpUndoStack || fixUpRedoStack; + var needsUndoOp = undoable || fixUp; var op, undoOp; if ((needsUndoOp && this.type.diffX) || !this.type.diff) { @@ -1006,68 +849,9 @@ Doc.prototype.submitSnapshot = function(snapshot, options, callback) { var submitOptions = { source: options && options.source, skipNoop: options && options.skipNoop, - operationType: undoable ? OPERATION_TYPES.UNDOABLE : OPERATION_TYPES.FIXED, + undoable: undoable, undoOp: undoOp, - fixUpUndoStack: fixUpUndoStack, - fixUpRedoStack: fixUpRedoStack - }; - this._submit(op, submitOptions, callback); -}; - -// Returns true, if there are any operations on the undo stack, otherwise false. -Doc.prototype.canUndo = function() { - return this.undoStack.length > 0 -}; - -// Undoes a submitted operation. -// -// @param options {source: ...} -// @param [callback] called after operation submitted -// @fires before op, op -Doc.prototype.undo = function(options, callback) { - if (typeof options === 'function') { - callback = options; - options = null; - } - - if (!this.canUndo()) { - if (callback) process.nextTick(callback); - return; - } - - var op = this.undoStack.pop(); - var submitOptions = { - source: options && options.source, - operationType: OPERATION_TYPES.UNDO - }; - this._submit(op, submitOptions, callback); -}; - -// Returns true, if there are any operations on the redo stack, otherwise false. -Doc.prototype.canRedo = function() { - return this.redoStack.length > 0 -}; - -// Redoes an undone operation. -// -// @param options {source:...} -// @param [callback] called after operation submitted -// @fires before op, op -Doc.prototype.redo = function(options, callback) { - if (typeof options === 'function') { - callback = options; - options = null; - } - - if (!this.canRedo()) { - if (callback) process.nextTick(callback); - return; - } - - var op = this.redoStack.pop() - var submitOptions = { - source: options && options.source, - operationType: OPERATION_TYPES.REDO + fixUp: fixUp }; this._submit(op, submitOptions, callback); }; diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js new file mode 100644 index 000000000..7f51218e2 --- /dev/null +++ b/lib/client/undoManager.js @@ -0,0 +1,273 @@ +function findLastIndex(stack, doc) { + var index = stack.length - 1; + while (index >= 0) { + if (stack[index].doc === doc) break; + index--; + } + return index; +} + +function getLast(list) { + var lastIndex = list.length - 1; + if (lastIndex < 0) throw new Error('List empty'); + return list[lastIndex]; +} + +function setLast(list, item) { + var lastIndex = list.length - 1; + if (lastIndex < 0) throw new Error('List empty'); + list[lastIndex] = item; +} + +function Item(op, doc) { + this.op = op; + this.doc = doc; +} + +// Manages an undo/redo stack for all operations from the specified `source`. +module.exports = UndoManager; +function UndoManager(connection, options) { + this.connection = connection; + + // The "source" value of undoable operations. If null or undefined, it works for all operations. + this._source = options && options.source; + + // The max number of undo operations to keep on the stack. + this._limit = options && typeof options.limit === 'number' ? options.limit : 100; + + // The max time difference between operations in milliseconds, + // which still allows the operations to be composed on the undoStack. + this._composeTimeout = options && typeof options.composeTimeout === 'number' ? options.composeTimeout : 1000; + + // Undo stack for local operations. + this._undoStack = []; + + // Redo stack for local operations. + this._redoStack = []; + + // The timestamp of the previous reversible operation. Used to determine if + // the next reversible operation can be composed on the undoStack. + this._previousUndoableOperationTime = -Infinity; + + // The type of operation that is currently in progress. + this._operationInProgress = null; +} + +UndoManager.prototype.destroy = function() { + this.connection.removeUndoManager(this); +}; + +UndoManager.prototype.clear = function() { + this._undoStack.length = 0; + this._redoStack.length = 0; + this._previousUndoableOperationTime = -Infinity; +}; + +// Returns true, if there are any operations on the undo stack, otherwise false. +UndoManager.prototype.canUndo = function() { + return this._undoStack.length > 0 +}; + +// Undoes a submitted operation. +// +// @param options {source: ...} +// @param [callback] called after operation submitted +// @fires before op, op +UndoManager.prototype.undo = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!this.canUndo()) { + if (callback) process.nextTick(callback); + return; + } + + this._operationInProgress = 'undo'; + var op = this._undoStack.pop(); + var submitOptions = { + source: options && options.source, + undoable: true + }; + op.doc._submit(op, submitOptions, callback); +}; + +// Returns true, if there are any operations on the redo stack, otherwise false. +UndoManager.prototype.canRedo = function() { + return this._redoStack.length > 0; +}; + +// Redoes an undone operation. +// +// @param options {source: ...} +// @param [callback] called after operation submitted +// @fires before op, op +UndoManager.prototype.redo = function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!this.canRedo()) { + if (callback) process.nextTick(callback); + return; + } + + this._operationInProgress = 'redo'; + var op = this._redoStack.pop(); + var submitOptions = { + source: options && options.source, + undoable: true + }; + op.doc._submit(op, submitOptions, callback); +}; + +UndoManager.prototype.onDocLoad = function(doc) { + this.clear(); +}; + +UndoManager.prototype.onDocCreate = function(doc) { + this.clear(); +}; + +UndoManager.prototype.onDocDelete = function(doc) { + this.clear(); +}; + +UndoManager.prototype.onDocOp = function(doc, op, undoOp, source, undoable, fixUp) { + if (this._operationInProgress === 'undo') { + this._updateStacksUndo(doc, op, undoOp); + this._operationInProgress = null; + + } else if (this._operationInProgress === 'redo') { + this._updateStacksRedo(doc, op, undoOp); + this._operationInProgress = null; + + } else if (!fixUp && undoable && (this._source == null || this._source === source)) { + this._updateStacksUndoable(doc, op, undoOp); + + } else { + this._updateStacksFixed(doc, op, undoOp, fixUp); + } +}; + +UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { + var now = Date.now(); + + if ( + this._undoStack.length === 0 || + getLast(this._undoStack).doc !== doc || + now - this._previousUndoableOperationTime > this._composeTimeout + ) { + this._undoStack.push(new Item(undoOp.op, doc)); + + } else if (doc.type.composeSimilar) { + var lastOp = getLast(this._undoStack); + var composedOp = doc.type.composeSimilar(undoOp.op, lastOp.op); + if (composedOp != null) { + setLast(this._undoStack, new Item(composedOp, doc)); + } else { + this._undoStack.push(new Item(undoOp.op, doc)); + } + + } else if (doc.type.compose) { + var lastOp = getLast(this._undoStack); + var composedOp = doc.type.compose(undoOp.op, lastOp.op); + setLast(this._undoStack, new Item(composedOp, doc)); + + } else { + this._undoStack.push(new Item(undoOp.op, doc)); + } + + this._redoStack.length = 0; + this._previousUndoableOperationTime = now; + + var isNoop = doc.type.isNoop; + if (isNoop && isNoop(getLast(this._undoStack).op)) { + this._undoStack.pop(); + } + + var itemsToRemove = this._undoStack.length - this._limit; + if (itemsToRemove > 0) { + this._undoStack.splice(0, itemsToRemove); + } +}; + +UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { + if (!doc.type.isNoop || !doc.type.isNoop(undoOp.op)) { + this._redoStack.push(new Item(undoOp.op, doc)); + } + this._previousUndoableOperationTime = -Infinity; +}; + +UndoManager.prototype._updateStacksRedo = function(doc, op, undoOp) { + if (!doc.type.isNoop || !doc.type.isNoop(undoOp.op)) { + this._undoStack.push(new Item(undoOp.op, doc)); + } + this._previousUndoableOperationTime = -Infinity; +}; + +UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { + var fixUpUndoStack = false; + var fixUpRedoStack = false; + + if (fixUp && undoOp && doc.type.compose) { + var lastUndoIndex = findLastIndex(this._undoStack, doc); + if (lastUndoIndex >= 0) { + var lastOp = this._undoStack[lastUndoIndex]; + var composedOp = doc.type.compose(undoOp.op, lastOp.op); + if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { + this._undoStack[lastUndoIndex] = new Item(composedOp, doc); + } else { + this._undoStack.splice(lastUndoIndex, 1); + } + } + + var lastRedoIndex = findLastIndex(this._redoStack, doc); + if (lastRedoIndex >= 0) { + var lastOp = this._redoStack[lastRedoIndex]; + var composedOp = doc.type.compose(undoOp.op, lastOp.op); + if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { + this._redoStack[lastRedoIndex] = new Item(composedOp, doc); + } else { + this._redoStack.splice(lastRedoIndex, 1); + } + } + + } else { + this._undoStack = this._transformStack(this._undoStack, doc, op.op); + this._redoStack = this._transformStack(this._redoStack, doc, op.op); + } +}; + +UndoManager.prototype._transformStack = function(stack, doc, op) { + var transform = doc.type.transform; + var transformX = doc.type.transformX; + var isNoop = doc.type.isNoop; + var newStack = []; + var newStackIndex = 0; + + for (var i = stack.length - 1; i >= 0; --i) { + var stackOp = stack[i].op; + var transformedStackOp; + var transformedOp; + + if (transformX) { + var result = transformX(op, stackOp); + transformedOp = result[0]; + transformedStackOp = result[1]; + } else { + transformedOp = transform(op, stackOp, 'left'); + transformedStackOp = transform(stackOp, op, 'right'); + } + + if (!isNoop || !isNoop(transformedStackOp)) { + newStack[newStackIndex++] = new Item(transformedStackOp, doc); + } + + op = transformedOp; + } + + return newStack.reverse(); +}; diff --git a/package-lock.json b/package-lock.json index 0af5c9b23..027e6b09c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -133,9 +133,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "camelcase": { @@ -304,9 +304,9 @@ "dev": true }, "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -337,9 +337,9 @@ "dev": true }, "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "dom-serializer": { @@ -548,16 +548,10 @@ "path-is-absolute": "^1.0.0" } }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "handlebars": { @@ -812,12 +806,6 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "jsonpointer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", @@ -882,80 +870,18 @@ "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", "dev": true }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "^3.0.0", - "lodash._basecreate": "^3.0.0", - "lodash._isiterateecall": "^3.0.0" - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "log-driver": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", "dev": true }, + "lolex": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", + "integrity": "sha512-Oo2Si3RMKV3+lV5MsSWplDQFoTClz/24S0MMHYcgGWWmFXr6TMlqcqk/l1GtH+d5wLBwNRiqGnwDRMirtFalJw==", + "dev": true + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -1021,55 +947,51 @@ } }, "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", + "glob": "7.1.2", + "growl": "1.10.5", "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "3.1.2" + "supports-color": "5.4.0" }, "dependencies": { - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.2", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^1.0.0" + "has-flag": "^3.0.0" } } } diff --git a/package.json b/package.json index 4b96159f5..055267de5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^3.2.0", + "lolex": "^2.7.1", + "mocha": "^5.2.0", "ot-text": "^1.0.1", "rich-text": "^3.1.0", "sharedb-mingo-memory": "^1.0.0-beta" diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..64bc51b97 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -598,7 +598,7 @@ describe('client submit', function() { }); }); - it('transforming pending op by server delete returns error', function(done) { + it('transforming pending op by server delete emits error', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -608,19 +608,21 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); doc.pause(); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc.on('error', function(err) { expect(err).ok(); + expect(err.code).to.equal(4017); expect(doc.version).equal(2); expect(doc.data).eql(undefined); done(); }); + doc.submitOp({p: ['age'], na: 1}); doc.fetch(); }); }); }); }); - it('transforming pending op by server create returns error', function(done) { + it('transforming pending op by server create emits error', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -632,12 +634,13 @@ describe('client submit', function() { doc2.create({age: 5}, function(err) { if (err) return done(err); doc.pause(); - doc.create({age: 9}, function(err) { + doc.on('error', function(err) { expect(err).ok(); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); done(); }); + doc.create({age: 9}); doc.fetch(); }); }); diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index b98a62a58..55a5bc0f5 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -1,4 +1,5 @@ var async = require('async'); +var lolex = require("lolex"); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); @@ -21,6 +22,7 @@ types.register(invertibleType.typeWithTransformX); describe('client undo/redo', function() { beforeEach(function() { + this.clock = lolex.install(); this.backend = new Backend(); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); @@ -30,215 +32,226 @@ describe('client undo/redo', function() { afterEach(function(done) { this.backend.close(done); + this.clock.uninstall(); }); - it('submits a fixed operation', function(allDone) { + it('submits a non-undoable operation', function(allDone) { + var undoManager = this.connection.undoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ]), function(done) { expect(this.doc.version).to.equal(2); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this) ], allDone); }); - it('receives a remote operation', function(allDone) { - async.series([ - this.doc.subscribe.bind(this.doc), - this.doc2.create.bind(this.doc2, { test: 5 }), - this.doc2.submitOp.bind(this.doc2, [ { p: [ 'test' ], na: 2 } ]), - setTimeout, - function(done) { - expect(this.doc.version).to.equal(2); - expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); - done(); - }.bind(this) - ], allDone); + it('receives a remote operation', function(done) { + var undoManager = this.connection.undoManager(); + this.doc2.preventCompose = true; + this.doc.on('op', function() { + expect(this.doc.version).to.equal(2); + expect(this.doc.data).to.eql({ test: 7 }); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + done(); + }.bind(this)); + this.doc.subscribe(function() { + this.doc2.create({ test: 5 }); + this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ]); + }.bind(this)); }); it('submits an undoable operation', function(allDone) { + var undoManager = this.connection.undoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), function(done) { expect(this.doc.version).to.equal(2); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this) ], allDone); }); it('undoes an operation', function(allDone) { + var undoManager = this.connection.undoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), - this.doc.undo.bind(this.doc), + undoManager.undo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(3); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(true); done(); }.bind(this) ], allDone); }); it('redoes an operation', function(allDone) { + var undoManager = this.connection.undoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(4); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this) ], allDone); }); it('performs a series of undo and redo operations', function(allDone) { + var undoManager = this.connection.undoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(8); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this) ], allDone); }); it('performs a series of undo and redo operations synchronously', function() { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }), this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }), expect(this.doc.data).to.eql({ test: 7 }); - this.doc.undo(), + undoManager.undo(), expect(this.doc.data).to.eql({ test: 5 }); - this.doc.redo(), + undoManager.redo(), expect(this.doc.data).to.eql({ test: 7 }); - this.doc.undo(), + undoManager.undo(), expect(this.doc.data).to.eql({ test: 5 }); - this.doc.redo(), + undoManager.redo(), expect(this.doc.data).to.eql({ test: 7 }); - this.doc.undo(), + undoManager.undo(), expect(this.doc.data).to.eql({ test: 5 }); - this.doc.redo(), + undoManager.redo(), expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); }); it('undoes one of two operations', function(allDone) { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), - this.doc.undo.bind(this.doc), + undoManager.undo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(4); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); done(); }.bind(this) ], allDone); }); it('undoes two of two operations', function(allDone) { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), - this.doc.undo.bind(this.doc), - this.doc.undo.bind(this.doc), + undoManager.undo.bind(undoManager), + undoManager.undo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(5); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(true); done(); }.bind(this) ], allDone); }); - it('reoes one of two operations', function(allDone) { - this.doc.undoComposeTimeout = -1; + it('redoes one of two operations', function(allDone) { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), - this.doc.undo.bind(this.doc), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), + undoManager.undo.bind(undoManager), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(6); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); done(); }.bind(this) ], allDone); }); - it('reoes two of two operations', function(allDone) { - this.doc.undoComposeTimeout = -1; + it('redoes two of two operations', function(allDone) { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 3 } ], { undoable: true }), - this.doc.undo.bind(this.doc), - this.doc.undo.bind(this.doc), - this.doc.redo.bind(this.doc), - this.doc.redo.bind(this.doc), + undoManager.undo.bind(undoManager), + undoManager.undo.bind(undoManager), + undoManager.redo.bind(undoManager), + undoManager.redo.bind(undoManager), function(done) { expect(this.doc.version).to.equal(7); expect(this.doc.data).to.eql({ test: 10 }); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this) ], allDone); }); it('calls undo, when canUndo is false', function(done) { - expect(this.doc.canUndo()).to.equal(false); - this.doc.undo(done); + var undoManager = this.connection.undoManager(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.undo(done); }); it('calls undo, when canUndo is false - no callback', function() { - expect(this.doc.canUndo()).to.equal(false); - this.doc.undo(); + var undoManager = this.connection.undoManager(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.undo(); }); it('calls redo, when canRedo is false', function(done) { - expect(this.doc.canRedo()).to.equal(false); - this.doc.redo(done); + var undoManager = this.connection.undoManager(); + expect(undoManager.canRedo()).to.equal(false); + undoManager.redo(done); }); it('calls redo, when canRedo is false - no callback', function() { - expect(this.doc.canRedo()).to.equal(false); - this.doc.redo(); + var undoManager = this.connection.undoManager(); + expect(undoManager.canRedo()).to.equal(false); + undoManager.redo(); }); it('preserves source on create', function(done) { @@ -268,24 +281,26 @@ describe('client undo/redo', function() { }); it('preserves source on undo', function(done) { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.on('op', function(op, source) { expect(source).to.equal('test source'); done(); }); - this.doc.undo({ source: 'test source' }); + undoManager.undo({ source: 'test source' }); }); it('preserves source on redo', function(done) { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.undo(); + undoManager.undo(); this.doc.on('op', function(op, source) { expect(source).to.equal('test source'); done(); }); - this.doc.redo({ source: 'test source' }); + undoManager.redo({ source: 'test source' }); }); it('has source=false on remote operations', function(done) { @@ -301,101 +316,106 @@ describe('client undo/redo', function() { }); it('composes undoable operations within time limit', function(done) { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); setTimeout(function() { this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); expect(this.doc.data).to.eql({ test: 10 }); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); done(); - }.bind(this), 2); + }.bind(this), 1000); + this.clock.runAll(); }); it('composes undoable operations correctly', function() { + var undoManager = this.connection.undoManager(); this.doc.create({ a: 1, b: 2 }); this.doc.submitOp([ { p: [ 'a' ], od: 1 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'b' ], od: 2 } ], { undoable: true }); expect(this.doc.data).to.eql({}); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); var opCalled = false; this.doc.once('op', function(op) { opCalled = true; expect(op).to.eql([ { p: [ 'b' ], oi: 2 }, { p: [ 'a' ], oi: 1 } ]); }); - this.doc.undo(); + undoManager.undo(); expect(opCalled).to.equal(true); expect(this.doc.data).to.eql({ a: 1, b: 2 }); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql({}); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('does not compose undoable operations outside time limit', function(done) { - this.doc.undoComposeTimeout = 1; + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); setTimeout(function () { this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); expect(this.doc.data).to.eql({ test: 10 }); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); + expect(undoManager.canUndo()).to.equal(true); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); done(); - }.bind(this), 3); + }.bind(this), 1001); + this.clock.runAll(); }); - it('does not compose undoable operations, if undoComposeTimeout < 0', function() { - this.doc.undoComposeTimeout = -1; + it('does not compose undoable operations, if composeTimeout < 0', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); expect(this.doc.data).to.eql({ test: 10 }); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); + expect(undoManager.canUndo()).to.equal(true); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); }); it('does not compose undoable operations, if type does not support compose nor composeSimilar', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.type.uri); this.doc.submitOp(2, { undoable: true }); expect(this.doc.data).to.equal(7); this.doc.submitOp(2, { undoable: true }); expect(this.doc.data).to.equal(9); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(7); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(5); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.equal(7); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(9); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('uses applyAndInvert, if available', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('two') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); this.doc.submitOp([ otRichText.Action.createInsertText('one') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); }); @@ -435,25 +455,18 @@ describe('client undo/redo', function() { this.doc.submitSnapshot([ { insert: 'abc' } ], { undoable: true }); }); - it('fails to submit with fixUpUndoStack, if type is not invertible', function(done) { - this.doc.create('two', otText.type.uri); - this.doc.on('error', done); - this.doc.submitOp([ 'one' ], { fixUpUndoStack: true }, function(err) { - expect(err.code).to.equal(4025); - done(); - }); - }); - - it('fails to submit with fixUpRedoStack, if type is not invertible', function(done) { + it('fails to submit with fixUp, if type is not invertible', function(done) { + var undoManager = this.connection.undoManager(); this.doc.create('two', otText.type.uri); this.doc.on('error', done); - this.doc.submitOp([ 'one' ], { fixUpRedoStack: true }, function(err) { + this.doc.submitOp([ 'one' ], { fixUp: true }, function(err) { expect(err.code).to.equal(4025); done(); }); }); it('composes similar operations', function() { + var undoManager = this.connection.undoManager(); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('one') @@ -463,57 +476,40 @@ describe('client undo/redo', function() { otRichText.Action.createInsertText('two') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); - expect(this.doc.canRedo()).to.equal(false); - this.doc.undo(); + expect(undoManager.canRedo()).to.equal(false); + undoManager.undo(); expect(this.doc.data).to.eql([]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('onetwo') ]); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('does not compose dissimilar operations', function() { - this.doc.create([ - otRichText.Action.createInsertText(' ') - ], otRichText.type.uri); + var undoManager = this.connection.undoManager(); + this.doc.create([ otRichText.Action.createInsertText(' ') ], otRichText.type.uri); - this.doc.submitOp([ - otRichText.Action.createRetain(1), - otRichText.Action.createInsertText('two') - ], { undoable: true }); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText(' two') - ]); + this.doc.submitOp([ otRichText.Action.createRetain(1), otRichText.Action.createInsertText('two') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText(' two') ]); - this.doc.submitOp([ - otRichText.Action.createInsertText('one') - ], { undoable: true }); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText('one two') - ]); - - this.doc.undo(); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText(' two') - ]); - - this.doc.undo(); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText(' ') - ]); - - this.doc.redo(); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText(' two') - ]); - - this.doc.redo(); - expect(this.doc.data).to.eql([ - otRichText.Action.createInsertText('one two') - ]); + this.doc.submitOp([ otRichText.Action.createInsertText('one') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('one two') ]); + + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText(' two') ]); + + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText(' ') ]); + + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText(' two') ]); + + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('one two') ]); }); it('does not add no-ops to the undo stack on undoable operation', function() { + var undoManager = this.connection.undoManager(); var opCalled = false; this.doc.create([ otRichText.Action.createInsertText('test', [ 'key', 'value' ]) ], otRichText.type.uri); this.doc.on('op', function(op, source) { @@ -523,164 +519,114 @@ describe('client undo/redo', function() { this.doc.submitOp([ otRichText.Action.createRetain(4, [ 'key', 'value' ]) ], { undoable: true }); expect(opCalled).to.equal(true); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test', [ 'key', 'value' ]) ]); - expect(this.doc.canUndo()).to.eql(false); - expect(this.doc.canRedo()).to.eql(false); + expect(undoManager.canUndo()).to.eql(false); + expect(undoManager.canRedo()).to.eql(false); }); it('limits the size of the undo stack', function() { - this.doc.undoLimit = 2; - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ limit: 2, composeTimeout: -1 }); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); expect(this.doc.data).to.eql({ test: 11 }); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); - expect(this.doc.canUndo()).to.equal(false); - this.doc.undo(); + expect(undoManager.canUndo()).to.equal(true); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 7 }); }); - it('limits the size of the undo stack, after adjusting the limit', function() { - this.doc.undoLimit = 100; - this.doc.undoComposeTimeout = -1; - this.doc.create({ test: 5 }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.undoLimit = 2; - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - expect(this.doc.data).to.eql({ test: 15 }); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); - expect(this.doc.canUndo()).to.equal(true); - this.doc.undo(); - expect(this.doc.canUndo()).to.equal(false); - this.doc.undo(); - expect(this.doc.data).to.eql({ test: 11 }); - }); - - it('does not limit the size of the stacks on undo and redo operations', function() { - this.doc.undoLimit = 100; - this.doc.undoComposeTimeout = -1; - this.doc.create({ test: 5 }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.undoLimit = 2; - expect(this.doc.data).to.eql({ test: 15 }); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - expect(this.doc.data).to.eql({ test: 5 }); - this.doc.redo(); - this.doc.redo(); - this.doc.redo(); - this.doc.redo(); - this.doc.redo(); - expect(this.doc.data).to.eql({ test: 15 }); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - this.doc.undo(); - expect(this.doc.data).to.eql({ test: 5 }); - }); - it('does not compose the next operation after undo', function() { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); - this.doc.undoComposeTimeout = -1; + this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed - this.doc.undoComposeTimeout = 1000; - this.doc.undo(); + undoManager.undo(); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // composed expect(this.doc.data).to.eql({ test: 11 }); - expect(this.doc.canUndo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); }); it('does not compose the next operation after undo and redo', function() { + var undoManager = this.connection.undoManager(); this.doc.create({ test: 5 }); - this.doc.undoComposeTimeout = -1; + this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed + this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed - this.doc.undoComposeTimeout = 1000; - this.doc.undo(); - this.doc.redo(); + undoManager.undo(); + undoManager.redo(); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // composed expect(this.doc.data).to.eql({ test: 13 }); - expect(this.doc.canUndo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 9 }); - expect(this.doc.canUndo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 7 }); - expect(this.doc.canUndo()).to.equal(true); + expect(undoManager.canUndo()).to.equal(true); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql({ test: 5 }); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); }); it('clears stacks on del', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.undo(); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(true); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); this.doc.del(); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('transforms the stacks by remote operations', function(done) { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc2.subscribe(); this.doc.subscribe(); - this.doc.undoComposeTimeout = -1; this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); - setTimeout(function() { + undoManager.undo(); + undoManager.undo(); + this.doc.whenNothingPending(function() { this.doc.once('op', function(op, source) { expect(source).to.equal(false); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); done(); }.bind(this)); @@ -689,30 +635,30 @@ describe('client undo/redo', function() { }); it('transforms the stacks by remote operations and removes no-ops', function(done) { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc2.subscribe(); this.doc.subscribe(); - this.doc.undoComposeTimeout = -1; this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); - setTimeout(function() { + undoManager.undo(); + undoManager.undo(); + this.doc.whenNothingPending(function() { this.doc.once('op', function(op, source) { expect(source).to.equal(false); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('24') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('124') ]); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); done(); }.bind(this)); this.doc2.submitOp([ otRichText.Action.createDelete(1) ]); @@ -720,100 +666,100 @@ describe('client undo/redo', function() { }); it('transforms the stacks by a local FIXED operation', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); + undoManager.undo(); + undoManager.undo(); this.doc.submitOp([ otRichText.Action.createInsertText('ABC') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); }); it('transforms the stacks by a local FIXED operation and removes no-ops', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); + undoManager.undo(); + undoManager.undo(); this.doc.submitOp([ otRichText.Action.createDelete(1) ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('4') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('24') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('124') ]); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('transforms the stacks using transform', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create(0, invertibleType.type.uri); this.doc.submitOp(1, { undoable: true }); this.doc.submitOp(10, { undoable: true }); this.doc.submitOp(100, { undoable: true }); this.doc.submitOp(1000, { undoable: true }); - this.doc.undo(); - this.doc.undo(); + undoManager.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(11); this.doc.submitOp(10000); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(10001); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(10000); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10001); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10011); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10111); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(11111); }); it('transforms the stacks using transformX', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create(0, invertibleType.typeWithTransformX.uri); this.doc.submitOp(1, { undoable: true }); this.doc.submitOp(10, { undoable: true }); this.doc.submitOp(100, { undoable: true }); this.doc.submitOp(1000, { undoable: true }); - this.doc.undo(); - this.doc.undo(); + undoManager.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(11); this.doc.submitOp(10000); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(10001); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(10000); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10001); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10011); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(10111); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(11111); }); @@ -871,283 +817,127 @@ describe('client undo/redo', function() { this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }, done); }); - describe('operationType', function() { - it('reports UNDOABLE operationType', function(done) { - var beforeOpCalled = false; - this.doc.create({ test: 5 }); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(true); - expect(operationType).to.equal('UNDOABLE'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(true); - expect(operationType).to.equal('UNDOABLE'); - done(); - }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + describe('fixup operations', function() { + beforeEach(function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + + this.assert = function(text) { + var expected = text ? [ otRichText.Action.createInsertText(text) ] : []; + expect(this.doc.data).to.eql(expected); + return this; + }; + this.submitOp = function(op, options) { + if (typeof op === 'string') { + this.doc.submitOp([ otRichText.Action.createInsertText(op) ], options); + } else if (op < 0) { + this.doc.submitOp([ otRichText.Action.createDelete(-op) ], options); + } else { + throw new Error('Invalid op'); + } + return this; + }; + this.submitSnapshot = function(snapshot, options) { + this.doc.submitSnapshot([ otRichText.Action.createInsertText(snapshot) ], options); + return this; + }; + this.undo = function() { + undoManager.undo(); + return this; + }; + this.redo = function() { + undoManager.redo(); + return this; + }; + + this.doc.create([], otRichText.type.uri); + this.submitOp('d', { undoable: true }).assert('d'); + this.submitOp('c', { undoable: true }).assert('cd'); + this.submitOp('b', { undoable: true }).assert('bcd'); + this.submitOp('a', { undoable: true }).assert('abcd'); + this.undo().assert('bcd'); + this.undo().assert('cd'); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); }); - it('reports UNDO operationType', function(done) { - var beforeOpCalled = false; - this.doc.create({ test: 5 }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(true); - expect(operationType).to.equal('UNDO'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(true); - expect(operationType).to.equal('UNDO'); - done(); - }); - this.doc.undo(); + it('submits an op and does not fix up stacks (insert)', function() { + this.submitOp('!').assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); }); - it('reports REDO operationType', function(done) { - var beforeOpCalled = false; - this.doc.create({ test: 5 }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.undo(); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(true); - expect(operationType).to.equal('REDO'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(true); - expect(operationType).to.equal('REDO'); - done(); - }); - this.doc.redo(); + it('submits an op and fixes up stacks (insert)', function() { + this.submitOp('!', { fixUp: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); }); - it('reports FIXED operationType (local operation, undoable=false)', function(done) { - var beforeOpCalled = false; - this.doc.create({ test: 5 }); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(true); - expect(operationType).to.equal('FIXED'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(true); - expect(operationType).to.equal('FIXED'); - done(); - }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ]); + it('submits a snapshot and does not fix up stacks (insert)', function() { + this.submitSnapshot('!cd').assert('!cd'); + this.undo().assert('!d'); + this.undo().assert('!'); + this.redo().assert('!d'); + this.redo().assert('!cd'); + this.redo().assert('!bcd'); + this.redo().assert('!abcd'); }); - it('reports FIXED operationType (remote operation, undoable=false)', function(done) { - var beforeOpCalled = false; - this.doc.subscribe(); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(false); - expect(operationType).to.equal('FIXED'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(false); - expect(operationType).to.equal('FIXED'); - done(); - }); - this.doc2.preventCompose = true; - this.doc2.create({ test: 5 }); - this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ]); + it('submits a snapshot and fixes up stacks (insert)', function() { + this.submitSnapshot('!cd', { fixUp: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); }); - it('reports FIXED operationType (remote operation, undoable=true)', function(done) { - var beforeOpCalled = false; - this.doc.subscribe(); - this.doc.on('before op', function(op, source, operationType) { - expect(source).to.equal(false); - expect(operationType).to.equal('FIXED'); - beforeOpCalled = true; - }); - this.doc.on('op', function(op, source, operationType) { - expect(beforeOpCalled).to.equal(true); - expect(source).to.equal(false); - expect(operationType).to.equal('FIXED'); - done(); - }); - this.doc2.preventCompose = true; - this.doc2.create({ test: 5 }); - this.doc2.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + it('submits an op and does not fix up stacks (delete)', function() { + this.submitOp(-1).assert('d'); + this.undo().assert(''); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('bd'); + this.redo().assert('abd'); + this.redo().assert('abd'); }); - }); - describe('fixup operations', function() { - describe('basic tests', function() { - beforeEach(function() { - this.assert = function(text) { - var expected = text ? [ otRichText.Action.createInsertText(text) ] : []; - expect(this.doc.data).to.eql(expected); - return this; - }; - this.submitOp = function(op, options) { - this.doc.submitOp([ otRichText.Action.createInsertText(op) ], options); - return this; - }; - this.submitSnapshot = function(snapshot, options) { - this.doc.submitSnapshot([ otRichText.Action.createInsertText(snapshot) ], options); - return this; - }; - this.undo = function() { - this.doc.undo(); - return this; - }; - this.redo = function() { - this.doc.redo(); - return this; - }; - - this.doc.undoComposeTimeout = -1; - this.doc.create([], otRichText.type.uri); - this.submitOp('d', { undoable: true }).assert('d'); - this.submitOp('c', { undoable: true }).assert('cd'); - this.submitOp('b', { undoable: true }).assert('bcd'); - this.submitOp('a', { undoable: true }).assert('abcd'); - this.undo().assert('bcd'); - this.undo().assert('cd'); - expect(this.doc.canUndo()).to.equal(true); - expect(this.doc.canRedo()).to.equal(true); - }); - - it('submits an operation (transforms undo stack, transforms redo stack)', function() { - this.submitOp('!').assert('!cd'); - this.undo().assert('!d'); - this.undo().assert('!'); - this.redo().assert('!d'); - this.redo().assert('!cd'); - this.redo().assert('!bcd'); - this.redo().assert('!abcd'); - }); - - it('submits an operation (fixes up undo stack, transforms redo stack)', function() { - this.submitOp('!', { fixUpUndoStack: true }).assert('!cd'); - this.undo().assert('d'); - this.undo().assert(''); - this.redo().assert('d'); - this.redo().assert('!cd'); - this.redo().assert('!bcd'); - this.redo().assert('!abcd'); - }); - - it('submits an operation (transforms undo stack, fixes up redo stack)', function() { - this.submitOp('!', { fixUpRedoStack: true }).assert('!cd'); - this.undo().assert('!d'); - this.undo().assert('!'); - this.redo().assert('!d'); - this.redo().assert('!cd'); - this.redo().assert('bcd'); - this.redo().assert('abcd'); - }); - - it('submits an operation (fixes up undo stack, fixes up redo stack)', function() { - this.submitOp('!', { fixUpUndoStack: true, fixUpRedoStack: true }).assert('!cd'); - this.undo().assert('d'); - this.undo().assert(''); - this.redo().assert('d'); - this.redo().assert('!cd'); - this.redo().assert('bcd'); - this.redo().assert('abcd'); - }); - - it('submits a snapshot (transforms undo stack, transforms redo stack)', function() { - this.submitSnapshot('!cd').assert('!cd'); - this.undo().assert('!d'); - this.undo().assert('!'); - this.redo().assert('!d'); - this.redo().assert('!cd'); - this.redo().assert('!bcd'); - this.redo().assert('!abcd'); - }); - - it('submits a snapshot (fixes up undo stack, transforms redo stack)', function() { - this.submitSnapshot('!cd', { fixUpUndoStack: true }).assert('!cd'); - this.undo().assert('d'); - this.undo().assert(''); - this.redo().assert('d'); - this.redo().assert('!cd'); - this.redo().assert('!bcd'); - this.redo().assert('!abcd'); - }); - - it('submits a snapshot (transforms undo stack, fixes up redo stack)', function() { - this.submitSnapshot('!cd', { fixUpRedoStack: true }).assert('!cd'); - this.undo().assert('!d'); - this.undo().assert('!'); - this.redo().assert('!d'); - this.redo().assert('!cd'); - this.redo().assert('bcd'); - this.redo().assert('abcd'); - }); - - it('submits a snapshot (fixes up undo stack, fixes up redo stack)', function() { - this.submitSnapshot('!cd', { fixUpUndoStack: true, fixUpRedoStack: true }).assert('!cd'); - this.undo().assert('d'); - this.undo().assert(''); - this.redo().assert('d'); - this.redo().assert('!cd'); - this.redo().assert('bcd'); - this.redo().assert('abcd'); - }); + it('submits an op and fixes up stacks (delete)', function() { + this.submitOp(-1, { fixUp: true }).assert('d'); + this.undo().assert(''); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + this.redo().assert('abcd'); }); - describe('no-ops', function() { - it('removes a no-op from the undo stack', function() { - this.doc.undoComposeTimeout = -1; - this.doc.create([], otRichText.type.uri); - this.doc.submitOp([ otRichText.Action.createInsertText('d') ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createInsertText('c') ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createInsertText('b') ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createInsertText('a') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('cd') ]); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { fixUpUndoStack: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); - this.doc.undo(); - expect(this.doc.data).to.eql([]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); - this.doc.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('bd') ]); - this.doc.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abd') ]); - expect(this.doc.canRedo()).to.equal(false); - }); + it('submits a snapshot and does not fix up stacks (delete)', function() { + this.submitSnapshot('d').assert('d'); + this.undo().assert(''); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('bd'); + this.redo().assert('abd'); + this.redo().assert('abd'); + }); - it('removes a no-op from the redo stack', function() { - this.doc.undoComposeTimeout = -1; - this.doc.create([ otRichText.Action.createInsertText('abcd') ], otRichText.type.uri); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('cd') ]); - this.doc.submitOp([ otRichText.Action.createDelete(1) ], { fixUpRedoStack: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); - this.doc.redo(); - expect(this.doc.data).to.eql([]); - expect(this.doc.canRedo()).to.equal(false); - this.doc.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('d') ]); - this.doc.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('bd') ]); - this.doc.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abd') ]); - expect(this.doc.canUndo()).to.equal(false); - }); + it('submits a snapshot and fixes up stacks (delete)', function() { + this.submitSnapshot('d', { fixUp: true }).assert('d'); + this.undo().assert(''); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + this.redo().assert('abcd'); }); }); @@ -1190,11 +980,12 @@ describe('client undo/redo', function() { }); it('submits a snapshot with source (no callback)', function(done) { + var undoManager = this.connection.undoManager(); this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); expect(source).to.equal('test'); done(); }.bind(this)); @@ -1203,13 +994,14 @@ describe('client undo/redo', function() { }); it('submits a snapshot with source (with callback)', function(done) { + var undoManager = this.connection.undoManager(); var opEmitted = false; this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); expect(source).to.equal('test'); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); opEmitted = true; }.bind(this)); this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); @@ -1220,11 +1012,12 @@ describe('client undo/redo', function() { }); it('submits a snapshot without source (no callback)', function(done) { + var undoManager = this.connection.undoManager(); this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); expect(source).to.equal(true); done(); }.bind(this)); @@ -1233,13 +1026,14 @@ describe('client undo/redo', function() { }); it('submits a snapshot without source (with callback)', function(done) { + var undoManager = this.connection.undoManager(); var opEmitted = false; this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); expect(source).to.equal(true); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); opEmitted = true; }.bind(this)); this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); @@ -1250,44 +1044,45 @@ describe('client undo/redo', function() { }); it('submits snapshots and supports undo and redo', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - expect(this.doc.canRedo()).to.equal(false); - this.doc.undo(); + expect(undoManager.canRedo()).to.equal(false); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); }); it('submits snapshots and composes operations', function() { + var undoManager = this.connection.undoManager(); this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(this.doc.canUndo()).to.equal(false); - this.doc.redo(); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - expect(this.doc.canRedo()).to.equal(false); - this.doc.undo(); + expect(undoManager.canRedo()).to.equal(false); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(this.doc.canUndo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); }); it('submits a snapshot and syncs it', function(done) { @@ -1306,7 +1101,7 @@ describe('client undo/redo', function() { }); it('submits undoable and fixed operations', function() { - this.doc.undoComposeTimeout = -1; + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('a') ], { undoable: true }); this.doc.submitSnapshot([ otRichText.Action.createInsertText('ab') ], { undoable: true }); @@ -1314,34 +1109,35 @@ describe('client undo/redo', function() { this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcd') ], { undoable: true }); this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcde') ], { undoable: true }); this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { undoable: true }); - this.doc.undo(); - this.doc.undo(); + undoManager.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcd') ]); this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc123d') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('123') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123de') ]); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123def') ]); }); it('submits a snapshot without a diffHint', function() { + var undoManager = this.connection.undoManager(); var opCalled = 0; this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true }); @@ -1352,19 +1148,20 @@ describe('client undo/redo', function() { expect(op).to.eql([ otRichText.Action.createDelete(1) ]); opCalled++; }.bind(this)); - this.doc.undo(); + undoManager.undo(); this.doc.once('op', function(op) { expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); expect(op).to.eql([ otRichText.Action.createInsertText('a') ]); opCalled++; }.bind(this)); - this.doc.redo(); + undoManager.redo(); expect(opCalled).to.equal(2); }); it('submits a snapshot with a diffHint', function() { + var undoManager = this.connection.undoManager(); var opCalled = 0; this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true, diffHint: 2 }); @@ -1375,14 +1172,14 @@ describe('client undo/redo', function() { expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createDelete(1) ]); opCalled++; }.bind(this)); - this.doc.undo(); + undoManager.undo(); this.doc.once('op', function(op) { expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createInsertText('a') ]); opCalled++; }.bind(this)); - this.doc.redo(); + undoManager.redo(); expect(opCalled).to.equal(2); }); @@ -1412,57 +1209,63 @@ describe('client undo/redo', function() { describe('with diff', function () { it('submits a snapshot (non-undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiff.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiff.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(5); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(7); }); }); describe('with diffX', function () { it('submits a snapshot (non-undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiffX.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiffX.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(5); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(7); }); }); describe('with diff and diffX', function () { it('submits a snapshot (non-undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); - expect(this.doc.canUndo()).to.equal(false); - expect(this.doc.canRedo()).to.equal(false); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { + var undoManager = this.connection.undoManager(); this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); - this.doc.undo(); + undoManager.undo(); expect(this.doc.data).to.equal(5); - this.doc.redo(); + undoManager.redo(); expect(this.doc.data).to.equal(7); }); }); From f289a58d54fe1092fe77c03ff36404915aa42c31 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 13:31:09 +0200 Subject: [PATCH 11/27] Simplify the code --- lib/client/doc.js | 16 ++++++++-------- lib/client/undoManager.js | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 0915e9f8b..10e7e9929 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -508,7 +508,7 @@ Doc.prototype._otApply = function(op, options) { var err = new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); return this.emit('error', err); } - var undoOp = options && options.undoOp || null; + var undoOp = options && options.undoOp && options.undoOp.op || null; var undoable = options && options.undoable || false; var fixUp = options && options.fixUp || false; @@ -542,7 +542,7 @@ Doc.prototype._otApply = function(op, options) { } // Apply the individual op component this.emit('before op', componentOp.op, source); - this._applyOp(componentOp, undoOp, source, undoable, fixUp); + this._applyOp(componentOp.op, undoOp, source, undoable, fixUp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -554,7 +554,7 @@ Doc.prototype._otApply = function(op, options) { // 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._applyOp(op, undoOp, source, undoable, fixUp); + this._applyOp(op.op, undoOp, source, undoable, fixUp); // Emit an '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. @@ -589,15 +589,15 @@ Doc.prototype._otApply = function(op, options) { Doc.prototype._applyOp = function(op, undoOp, source, undoable, fixUp) { if (undoOp == null && (undoable || fixUp)) { if (this.type.applyAndInvert) { - var result = this.type.applyAndInvert(this.data, op.op); + var result = this.type.applyAndInvert(this.data, op); this.data = result[0]; - undoOp = { op: result[1] }; + undoOp = result[1]; } else { - this.data = this.type.apply(this.data, op.op); - undoOp = { op: this.type.invert(op.op) }; + this.data = this.type.apply(this.data, op); + undoOp = this.type.invert(op); } } else { - this.data = this.type.apply(this.data, op.op); + this.data = this.type.apply(this.data, op); } this.connection.onDocOp(this, op, undoOp, source, undoable, fixUp); diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index 7f51218e2..fa463a7b3 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -160,24 +160,24 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { getLast(this._undoStack).doc !== doc || now - this._previousUndoableOperationTime > this._composeTimeout ) { - this._undoStack.push(new Item(undoOp.op, doc)); + this._undoStack.push(new Item(undoOp, doc)); } else if (doc.type.composeSimilar) { var lastOp = getLast(this._undoStack); - var composedOp = doc.type.composeSimilar(undoOp.op, lastOp.op); + var composedOp = doc.type.composeSimilar(undoOp, lastOp.op); if (composedOp != null) { setLast(this._undoStack, new Item(composedOp, doc)); } else { - this._undoStack.push(new Item(undoOp.op, doc)); + this._undoStack.push(new Item(undoOp, doc)); } } else if (doc.type.compose) { var lastOp = getLast(this._undoStack); - var composedOp = doc.type.compose(undoOp.op, lastOp.op); + var composedOp = doc.type.compose(undoOp, lastOp.op); setLast(this._undoStack, new Item(composedOp, doc)); } else { - this._undoStack.push(new Item(undoOp.op, doc)); + this._undoStack.push(new Item(undoOp, doc)); } this._redoStack.length = 0; @@ -195,15 +195,15 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { }; UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { - if (!doc.type.isNoop || !doc.type.isNoop(undoOp.op)) { - this._redoStack.push(new Item(undoOp.op, doc)); + if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { + this._redoStack.push(new Item(undoOp, doc)); } this._previousUndoableOperationTime = -Infinity; }; UndoManager.prototype._updateStacksRedo = function(doc, op, undoOp) { - if (!doc.type.isNoop || !doc.type.isNoop(undoOp.op)) { - this._undoStack.push(new Item(undoOp.op, doc)); + if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { + this._undoStack.push(new Item(undoOp, doc)); } this._previousUndoableOperationTime = -Infinity; }; @@ -216,7 +216,7 @@ UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { var lastUndoIndex = findLastIndex(this._undoStack, doc); if (lastUndoIndex >= 0) { var lastOp = this._undoStack[lastUndoIndex]; - var composedOp = doc.type.compose(undoOp.op, lastOp.op); + var composedOp = doc.type.compose(undoOp, lastOp.op); if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { this._undoStack[lastUndoIndex] = new Item(composedOp, doc); } else { @@ -227,7 +227,7 @@ UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { var lastRedoIndex = findLastIndex(this._redoStack, doc); if (lastRedoIndex >= 0) { var lastOp = this._redoStack[lastRedoIndex]; - var composedOp = doc.type.compose(undoOp.op, lastOp.op); + var composedOp = doc.type.compose(undoOp, lastOp.op); if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { this._redoStack[lastRedoIndex] = new Item(composedOp, doc); } else { @@ -236,8 +236,8 @@ UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { } } else { - this._undoStack = this._transformStack(this._undoStack, doc, op.op); - this._redoStack = this._transformStack(this._redoStack, doc, op.op); + this._undoStack = this._transformStack(this._undoStack, doc, op); + this._redoStack = this._transformStack(this._redoStack, doc, op); } }; From 498ee3f149ff61c6e878378b03925ccab07199d8 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 13:41:03 +0200 Subject: [PATCH 12/27] Document connection.undoManager(options) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a61f38968..fd0a9631a 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,12 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance. * `options.*` All other options are passed through to the database adapter. +`connection.undoManager(options)` creates a new `UndoManager`. + +* `options.source` if specified, only the operations from that `source` will be undo-able. If `null` or `undefined`, the `source` filter is disabled. +* `options.limit` the max number of operations to keep on the undo stack. +* `options.composeTimeout` the max time difference between operations in milliseconds, which still allows the operations to be composed on the undoStack. + ### Class: `ShareDB.Doc` `doc.type` _(String)_ From 1eb1d3ce95af87672683aba7c40ec3f5a360e7de Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 16:42:29 +0200 Subject: [PATCH 13/27] Fix some issues --- lib/client/undoManager.js | 48 ++++++++--- test/client/undo-redo.js | 175 +++++++++++++++++++++++++++++++++++--- 2 files changed, 199 insertions(+), 24 deletions(-) diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index fa463a7b3..f571cbb18 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -27,9 +27,10 @@ function Item(op, doc) { // Manages an undo/redo stack for all operations from the specified `source`. module.exports = UndoManager; function UndoManager(connection, options) { - this.connection = connection; + // The Connection which created this UndoManager. + this._connection = connection; - // The "source" value of undoable operations. If null or undefined, it works for all operations. + // If != null, only ops from this "source" will be undoable. this._source = options && options.source; // The max number of undo operations to keep on the stack. @@ -50,17 +51,27 @@ function UndoManager(connection, options) { this._previousUndoableOperationTime = -Infinity; // The type of operation that is currently in progress. + // It depends on the `op` event being triggered synchronously when submitting an operation or snapshot. this._operationInProgress = null; } UndoManager.prototype.destroy = function() { - this.connection.removeUndoManager(this); + this._connection.removeUndoManager(this); + this.clear(); }; -UndoManager.prototype.clear = function() { - this._undoStack.length = 0; - this._redoStack.length = 0; - this._previousUndoableOperationTime = -Infinity; +// Clear the undo and redo stack. +// +// @param doc If specified, clear only the ops belonging to this doc. +UndoManager.prototype.clear = function(doc) { + if (doc) { + var filter = function(item) { return item.doc !== doc; }; + this._undoStack = this._undoStack.filter(filter); + this._redoStack = this._redoStack.filter(filter); + } else { + this._undoStack.length = 0; + this._redoStack.length = 0; + } }; // Returns true, if there are any operations on the undo stack, otherwise false. @@ -124,15 +135,25 @@ UndoManager.prototype.redo = function(options, callback) { }; UndoManager.prototype.onDocLoad = function(doc) { - this.clear(); + this.clear(doc); }; UndoManager.prototype.onDocCreate = function(doc) { - this.clear(); + // NOTE We don't support undo on create because we can't support undo on delete. }; UndoManager.prototype.onDocDelete = function(doc) { - this.clear(); + // NOTE We can't support undo on delete because we can't generate `initialData` required for `create`. + // See https://github.com/ottypes/docs#standard-properties. + // + // We could support undo on delete and create in the future but that would require some breaking changes to ShareDB. + // Here's what we could do: + // + // 1. Do NOT call `create` in ShareDB - ShareDB would get a valid snapshot from the client code. + // 2. Add `validate` to OT types. + // 3. Call `validate` in ShareDB to ensure that the snapshot from the client is valid. + // 4. The `create` ops would contain serialized snapshots instead of `initialData`. + this.clear(doc); }; UndoManager.prototype.onDocOp = function(doc, op, undoOp, source, undoable, fixUp) { @@ -249,7 +270,12 @@ UndoManager.prototype._transformStack = function(stack, doc, op) { var newStackIndex = 0; for (var i = stack.length - 1; i >= 0; --i) { - var stackOp = stack[i].op; + var item = stack[i]; + if (item.doc !== doc) { + newStack[newStackIndex++] = item; + continue; + } + var stackOp = item.op; var transformedStackOp; var transformedOp; diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 55a5bc0f5..e5d8444a8 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -588,19 +588,6 @@ describe('client undo/redo', function() { expect(undoManager.canUndo()).to.equal(false); }); - it('clears stacks on del', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); - this.doc.create({ test: 5 }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); - undoManager.undo(); - expect(undoManager.canUndo()).to.equal(true); - expect(undoManager.canRedo()).to.equal(true); - this.doc.del(); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - }); - it('transforms the stacks by remote operations', function(done) { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc2.subscribe(); @@ -941,6 +928,168 @@ describe('client undo/redo', function() { }); }); + describe('UndoManager.clear', function() { + it('clears the stacks', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create({ test: 5 }); + doc2.create({ test: 11 }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + undoManager.clear(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + + it('clears the stacks for a specific document', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create({ test: 5 }); + doc2.create({ test: 11 }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + + undoManager.clear(doc1); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(true); + expect(doc1.data).to.eql({ test: 7 }); + expect(doc2.data).to.eql({ test: 11 }); + + undoManager.redo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + expect(doc1.data).to.eql({ test: 7 }); + expect(doc2.data).to.eql({ test: 13 }); + + undoManager.redo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); + expect(doc1.data).to.eql({ test: 7 }); + expect(doc2.data).to.eql({ test: 15 }); + }); + + it('clears the stacks for a specific document on del', function() { + // NOTE we don't support undo/redo on del/create at the moment. + // See undoManager.js for more details. + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create({ test: 5 }); + doc2.create({ test: 11 }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + doc1.del(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + doc2.del(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + + it('clears the stacks for a specific document on load', function(done) { + var shouldReject = false; + this.backend.use('submit', function(request, next) { + if (shouldReject) return next(request.rejectedError()); + next(); + }); + + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create([], otRichText.type.uri); + doc2.create([], otRichText.type.uri); + doc1.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true }); + doc2.submitOp([ otRichText.Action.createInsertText('b') ], { undoable: true }); + doc1.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true }); + doc2.submitOp([ otRichText.Action.createInsertText('a') ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + + this.connection.whenNothingPending(function() { + shouldReject = true; + doc1.submitOp([ otRichText.Action.createInsertText('!') ], function(err) { + if (err) return done(err); + shouldReject = false; + expect(doc1.data).to.eql([ otRichText.Action.createInsertText('2') ]); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(true); + expect(doc1.data).to.eql([ otRichText.Action.createInsertText('2') ]); + expect(doc2.data).to.eql([]); + + undoManager.redo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + expect(doc1.data).to.eql([ otRichText.Action.createInsertText('2') ]); + expect(doc2.data).to.eql([ otRichText.Action.createInsertText('b') ]); + + undoManager.redo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(false); + expect(doc1.data).to.eql([ otRichText.Action.createInsertText('2') ]); + expect(doc2.data).to.eql([ otRichText.Action.createInsertText('ab') ]); + + done(); + }); + }.bind(this)); + }); + + it.skip('clears the stacks for a specific document on doc destroy', function(done) { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create({ test: 5 }); + doc2.create({ test: 11 }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + doc1.destroy(function(err) { + if (err) return done(err); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + doc2.destroy(function(err) { + if (err) return done(err); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + }); + }); + }); + describe('submitSnapshot', function() { describe('basic tests', function() { it('submits a snapshot when document is not created (no callback, no options)', function(done) { From 1f9cb2e9605fbd918e84d8b788f97861384cbfe3 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 16:49:27 +0200 Subject: [PATCH 14/27] Clear undo/redo stacks on doc destroy --- lib/client/connection.js | 6 ++++++ lib/client/doc.js | 2 ++ lib/client/undoManager.js | 4 ++++ test/client/undo-redo.js | 3 ++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 97b8c00ba..fe99a0048 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -608,6 +608,12 @@ Connection.prototype.onDocLoad = function(doc) { } }; +Connection.prototype.onDocDestroy = function(doc) { + for (var i = 0; i < this.undoManagers.length; i++) { + this.undoManagers[i].onDocDestroy(doc); + } +}; + Connection.prototype.onDocCreate = function(doc) { for (var i = 0; i < this.undoManagers.length; i++) { this.undoManagers[i].onDocCreate(doc); diff --git a/lib/client/doc.js b/lib/client/doc.js index d482da448..2df6b0242 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -112,10 +112,12 @@ Doc.prototype.destroy = function(callback) { return; } doc.connection._destroyDoc(doc); + doc.connection.onDocDestroy(doc); if (callback) callback(); }); } else { doc.connection._destroyDoc(doc); + doc.connection.onDocDestroy(doc); if (callback) callback(); } }); diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index f571cbb18..98f9c8827 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -138,6 +138,10 @@ UndoManager.prototype.onDocLoad = function(doc) { this.clear(doc); }; +UndoManager.prototype.onDocDestroy = function(doc) { + this.clear(doc); +}; + UndoManager.prototype.onDocCreate = function(doc) { // NOTE We don't support undo on create because we can't support undo on delete. }; diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index e5d8444a8..5834656d5 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -1063,7 +1063,7 @@ describe('client undo/redo', function() { }.bind(this)); }); - it.skip('clears the stacks for a specific document on doc destroy', function(done) { + it('clears the stacks for a specific document on doc destroy', function(done) { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); @@ -1085,6 +1085,7 @@ describe('client undo/redo', function() { if (err) return done(err); expect(undoManager.canUndo()).to.equal(false); expect(undoManager.canRedo()).to.equal(false); + done(); }); }); }); From 563bc80251ac75ee492607755a617b738006efed Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 16:57:16 +0200 Subject: [PATCH 15/27] Add test for undoManager.destroy() --- test/client/undo-redo.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 5834656d5..a661ca79c 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -928,6 +928,34 @@ describe('client undo/redo', function() { }); }); + it('destroys UndoManager', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var doc1 = this.connection.get('dogs', 'fido'); + var doc2 = this.connection.get('dogs', 'toby'); + doc1.create({ test: 5 }); + doc2.create({ test: 11 }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + undoManager.destroy(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + expect(doc1.data).to.eql({ test: 11 }); + undoManager.undo(); + expect(doc1.data).to.eql({ test: 11 }); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + describe('UndoManager.clear', function() { it('clears the stacks', function() { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); @@ -941,11 +969,18 @@ describe('client undo/redo', function() { doc2.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); undoManager.undo(); undoManager.undo(); + expect(doc1.data).to.eql({ test: 7 }); expect(undoManager.canUndo()).to.equal(true); expect(undoManager.canRedo()).to.equal(true); undoManager.clear(); expect(undoManager.canUndo()).to.equal(false); expect(undoManager.canRedo()).to.equal(false); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + doc1.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + expect(doc1.data).to.eql({ test: 9 }); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); }); it('clears the stacks for a specific document', function() { From 11ec724155d72be5500311daf6651c0df8cdf9fe Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 17:26:01 +0200 Subject: [PATCH 16/27] Add more tests --- lib/client/undoManager.js | 7 ++++--- test/client/undo-redo.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index 98f9c8827..d874d9762 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -9,12 +9,14 @@ function findLastIndex(stack, doc) { function getLast(list) { var lastIndex = list.length - 1; + /* istanbul ignore if */ if (lastIndex < 0) throw new Error('List empty'); return list[lastIndex]; } function setLast(list, item) { var lastIndex = list.length - 1; + /* istanbul ignore if */ if (lastIndex < 0) throw new Error('List empty'); list[lastIndex] = item; } @@ -220,6 +222,7 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { }; UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { + /* istanbul ignore else */ if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { this._redoStack.push(new Item(undoOp, doc)); } @@ -227,6 +230,7 @@ UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { }; UndoManager.prototype._updateStacksRedo = function(doc, op, undoOp) { + /* istanbul ignore else */ if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { this._undoStack.push(new Item(undoOp, doc)); } @@ -234,9 +238,6 @@ UndoManager.prototype._updateStacksRedo = function(doc, op, undoOp) { }; UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { - var fixUpUndoStack = false; - var fixUpRedoStack = false; - if (fixUp && undoOp && doc.type.compose) { var lastUndoIndex = findLastIndex(this._undoStack, doc); if (lastUndoIndex >= 0) { diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index a661ca79c..1a44e482f 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -847,6 +847,15 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(true); }); + it('does not fix up anything', function() { + var undoManager = this.connection.undoManager(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + this.submitOp('!', { fixUp: true }).assert('!cd'); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + it('submits an op and does not fix up stacks (insert)', function() { this.submitOp('!').assert('!cd'); this.undo().assert('!d'); @@ -926,6 +935,21 @@ describe('client undo/redo', function() { this.redo().assert('abcd'); this.redo().assert('abcd'); }); + + it('submits a op and fixes up stacks (redo up becomes no-op and is removed from the stack)', function() { + this.redo().redo().assert('abcd'); + this.submitOp(-1, { undoable: true }).assert('bcd'); + this.submitOp(-1, { undoable: true }).assert('cd'); + this.submitOp(-1, { undoable: true }).assert('d'); + this.submitOp(-1, { undoable: true }).assert(''); + this.undo().undo().assert('cd'); + this.submitOp(-1, { fixUp: true }).assert('d'); + this.redo().assert(''); + this.redo().assert(''); + this.undo().assert('d'); + this.undo().assert('bcd'); + this.undo().assert('abcd'); + }); }); it('destroys UndoManager', function() { From 33be4b9571b285c3b4a6a0d262e28a8798462910 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Jul 2018 17:41:43 +0200 Subject: [PATCH 17/27] Add a test --- test/client/undo-redo.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 1a44e482f..66f632600 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -936,7 +936,7 @@ describe('client undo/redo', function() { this.redo().assert('abcd'); }); - it('submits a op and fixes up stacks (redo up becomes no-op and is removed from the stack)', function() { + it('submits a op and fixes up stacks (redo op becomes no-op and is removed from the stack)', function() { this.redo().redo().assert('abcd'); this.submitOp(-1, { undoable: true }).assert('bcd'); this.submitOp(-1, { undoable: true }).assert('cd'); @@ -950,6 +950,35 @@ describe('client undo/redo', function() { this.undo().assert('bcd'); this.undo().assert('abcd'); }); + + it('fixes up the correct ops', function() { + var doc = this.connection.get('dogs', 'toby'); + this.submitSnapshot('', { undoable: true }).assert(''); + this.submitSnapshot('d', { undoable: true }).assert('d'); + this.submitSnapshot('cd', { undoable: true }).assert('cd'); + doc.create({ test: 5 }); + doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); + this.submitSnapshot('bcd', { undoable: true }).assert('bcd'); + this.submitSnapshot('abcd', { undoable: true }).assert('abcd'); + this.undo().assert('bcd'); + this.undo().assert('cd'); + this.undo().assert('cd'); // undo one of the `doc` ops + expect(doc.data).to.eql({ test: 7 }); + this.submitSnapshot('!cd', { fixUp: true }).assert('!cd'); + this.undo().assert('!cd'); // undo one of the `doc` ops + expect(doc.data).to.eql({ test: 5 }); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('!cd'); // redo one of the `doc` ops + expect(doc.data).to.eql({ test: 7 }); + this.redo().assert('!cd'); // redo one of the `doc` ops + expect(doc.data).to.eql({ test: 9 }); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); }); it('destroys UndoManager', function() { From 14e5180fd1bdc27882167a47ce1f52f46f1ad570 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 15:20:53 +0200 Subject: [PATCH 18/27] Update mocha and fix 2 tests See https://github.com/mochajs/mocha/releases/tag/v5.0.2 --- package.json | 2 +- test/client/submit.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 35fc64bc6..4e38aba84 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^3.2.0", + "mocha": "^5.2.0", "sharedb-mingo-memory": "^1.0.0-beta" }, "scripts": { diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..ee3ae3e0d 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -608,11 +608,16 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); doc.pause(); + var calledBack = false; + doc.on('error', function(err) { + expect(calledBack).equal(true); + done(); + }); doc.submitOp({p: ['age'], na: 1}, function(err) { expect(err).ok(); expect(doc.version).equal(2); expect(doc.data).eql(undefined); - done(); + calledBack = true; }); doc.fetch(); }); @@ -632,11 +637,16 @@ describe('client submit', function() { doc2.create({age: 5}, function(err) { if (err) return done(err); doc.pause(); + var calledBack = false; + doc.on('error', function() { + expect(calledBack).equal(true); + done(); + }); doc.create({age: 9}, function(err) { expect(err).ok(); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); - done(); + calledBack = true; }); doc.fetch(); }); From e45dcc9ffa415a2afd845d161b1c051c9380919b Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:05:07 +0200 Subject: [PATCH 19/27] Fix sharedb does not exist --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bd066b20..736e5fe78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - "10" - - "9" - "8" - "6" - - "4" -script: "npm run jshint && npm run test-cover" +script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From d5a03f3c2c1f1b72a67d3c76e6816972061fa773 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:40:39 +0200 Subject: [PATCH 20/27] Remove unused variable --- test/client/submit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client/submit.js b/test/client/submit.js index ee3ae3e0d..6523880a8 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -609,7 +609,7 @@ describe('client submit', function() { if (err) return done(err); doc.pause(); var calledBack = false; - doc.on('error', function(err) { + doc.on('error', function() { expect(calledBack).equal(true); done(); }); From d09e506c687a3d2983adf4d7e1febd63436f0215 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:48:56 +0200 Subject: [PATCH 21/27] Update dependencies --- package-lock.json | 1450 --------------------------------------------- package.json | 6 +- 2 files changed, 3 insertions(+), 1453 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 027e6b09c..000000000 --- a/package-lock.json +++ /dev/null @@ -1,1450 +0,0 @@ -{ - "name": "sharedb", - "version": "1.0.0-beta.9", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@teamwork/ot-rich-text": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@teamwork/ot-rich-text/-/ot-rich-text-6.3.3.tgz", - "integrity": "sha512-cYHZPTRMY6N7GxJ3SENzHyGVtgLlDfMdtfRs1CG+6+6DzUkfP/VkEIoR+K5jp02DyI5yvMErkyJkv4BvM23sLg==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arraydiff": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", - "integrity": "sha1-hqVDbXty8b3aX9bXTock5C+Dzk0=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dev": true, - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "coveralls": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", - "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", - "dev": true, - "requires": { - "js-yaml": "3.6.1", - "lcov-parse": "0.0.10", - "log-driver": "1.2.5", - "minimist": "1.2.0", - "request": "2.79.0" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.x.x" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "optional": true - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", - "dev": true - } - } - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expect.js": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", - "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-diff": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", - "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "hat": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", - "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jshint": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz", - "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=", - "dev": true, - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "3.7.x", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz", - "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", - "dev": true - }, - "log-driver": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", - "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", - "dev": true - }, - "lolex": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.1.tgz", - "integrity": "sha512-Oo2Si3RMKV3+lV5MsSWplDQFoTClz/24S0MMHYcgGWWmFXr6TMlqcqk/l1GtH+d5wLBwNRiqGnwDRMirtFalJw==", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "make-error": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", - "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "requires": { - "mime-db": "~1.33.0" - } - }, - "mingo": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mingo/-/mingo-2.2.2.tgz", - "integrity": "sha1-vmnUhq5uCsVLl53F9EEtshhR9pM=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ot-json0": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", - "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" - }, - "ot-text": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ot-text/-/ot-text-1.0.1.tgz", - "integrity": "sha1-P4UPbuhYvDbvRayapR0Gx354388=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", - "dev": true - }, - "quill-delta": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.2.tgz", - "integrity": "sha512-grWEQq9woEidPDogtDNxQKmy2LFf9zBC0EU/YTSw6TwKmMjtihTxdnPtPRfrqazB2MSJ7YdCWxmsJ7aQKRSEgg==", - "dev": true, - "requires": { - "deep-equal": "^1.0.1", - "extend": "^3.0.1", - "fast-diff": "1.1.2" - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "rich-text": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rich-text/-/rich-text-3.1.0.tgz", - "integrity": "sha1-BMlx3tzo64IBDPrP9uegzjXHqCU=", - "dev": true, - "requires": { - "quill-delta": "^3.2.0" - } - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sharedb": { - "version": "1.0.0-beta.9", - "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-1.0.0-beta.9.tgz", - "integrity": "sha1-LX20J83hIJJNLasIzpLZq6134As=", - "dev": true, - "requires": { - "arraydiff": "^0.1.1", - "async": "^1.4.2", - "deep-is": "^0.1.3", - "hat": "0.0.3", - "make-error": "^1.1.1", - "ot-json0": "^1.0.1" - } - }, - "sharedb-mingo-memory": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sharedb-mingo-memory/-/sharedb-mingo-memory-1.0.0.tgz", - "integrity": "sha1-vS5171YTCrheE5uMlMVSFv4+TQM=", - "dev": true, - "requires": { - "mingo": "^2.2.0", - "sharedb": "^1.0.0-beta" - } - }, - "shelljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", - "dev": true - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "optional": true - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } -} diff --git a/package.json b/package.json index 055267de5..2e34445e8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "lib/index.js", "dependencies": { "arraydiff": "^0.1.1", - "async": "^1.4.2", + "async": "^2.6.1", "deep-is": "^0.1.3", "hat": "0.0.3", "make-error": "^1.1.1", @@ -13,7 +13,7 @@ }, "devDependencies": { "@teamwork/ot-rich-text": "^6.3.3", - "coveralls": "^2.11.8", + "coveralls": "^3.0.2", "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", @@ -21,7 +21,7 @@ "mocha": "^5.2.0", "ot-text": "^1.0.1", "rich-text": "^3.1.0", - "sharedb-mingo-memory": "^1.0.0-beta" + "sharedb-mingo-memory": "^1.0.1" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run jshint", From bfba7c0c346bc96e0c464c04d27f2dcad1f83ea7 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:59:01 +0200 Subject: [PATCH 22/27] Clean up after merge conflict --- test/client/submit.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/client/submit.js b/test/client/submit.js index 6b49f12d7..69a98aabd 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -620,7 +620,6 @@ describe('client submit', function() { expect(doc.data).eql(undefined); calledBack = true; }); - doc.submitOp({p: ['age'], na: 1}); doc.fetch(); }); }); @@ -650,7 +649,6 @@ describe('client submit', function() { expect(doc.data).eql({age: 5}); calledBack = true; }); - doc.create({age: 9}); doc.fetch(); }); }); From c7170d711f6a1fa4e070b5cb49fe4c9b42062825 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 22:29:33 +0200 Subject: [PATCH 23/27] Transform by undo and redo ops --- lib/client/doc.js | 26 +++-- lib/client/undoManager.js | 61 +++++------ test/client/undo-redo.js | 218 +++++++++++++++++++++++++++++++++++++- 3 files changed, 253 insertions(+), 52 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 2df6b0242..a79a956d9 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -519,7 +519,7 @@ Doc.prototype._otApply = function(op, options) { var err = new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); return this.emit('error', err); } - var undoOp = options && options.undoOp && options.undoOp.op || null; + var undoOp = options && options.undoOp || null; var undoable = options && options.undoable || false; var fixUp = options && options.fixUp || false; @@ -553,7 +553,7 @@ Doc.prototype._otApply = function(op, options) { } // Apply the individual op component this.emit('before op', componentOp.op, source); - this._applyOp(componentOp.op, undoOp, source, undoable, fixUp); + this._applyOp(componentOp, undoOp, source, undoable, fixUp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -565,7 +565,7 @@ Doc.prototype._otApply = function(op, options) { // 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._applyOp(op.op, undoOp, source, undoable, fixUp); + this._applyOp(op, undoOp, source, undoable, fixUp); // Emit an '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. @@ -598,17 +598,17 @@ Doc.prototype._otApply = function(op, options) { // Applies `op` to `this.data` and updates the undo/redo stacks. Doc.prototype._applyOp = function(op, undoOp, source, undoable, fixUp) { - if (undoOp == null && (undoable || fixUp)) { + if (undoOp == null && (undoable || fixUp || op.needsUndoOp)) { if (this.type.applyAndInvert) { - var result = this.type.applyAndInvert(this.data, op); + var result = this.type.applyAndInvert(this.data, op.op); this.data = result[0]; - undoOp = result[1]; + undoOp = { op: result[1] }; } else { - this.data = this.type.apply(this.data, op); - undoOp = this.type.invert(op); + this.data = this.type.apply(this.data, op.op); + undoOp = { op: this.type.invert(op.op) }; } } else { - this.data = this.type.apply(this.data, op); + this.data = this.type.apply(this.data, op.op); } this.connection.onDocOp(this, op, undoOp, source, undoable, fixUp); @@ -682,7 +682,7 @@ Doc.prototype._submit = function(op, options, callback) { } var undoable = options && options.undoable; var fixUp = options && options.fixUp; - var needsUndoOp = undoable || fixUp; + var needsUndoOp = undoable || fixUp || op.needsUndoOp; if (needsUndoOp && !this.type.invert && !this.type.applyAndInvert) { var err = new ShareDBError(4025, 'Cannot submit op. OT type does not support invert not applyAndInvert. ' + this.collection + '.' + this.id); if (callback) return callback(err); @@ -789,8 +789,7 @@ Doc.prototype._tryCompose = function(op) { // @param options.skipNoop should processing be skipped entirely, if `component` is a no-op. // @param options.undoable should the operation be undoable // @param options.fixUp If true, this operation is meant to fix the current invalid state of the snapshot. -// It also updates UndoManagers in such a way that undo/redo will "skip" over the current state of the snapshot. -// This feature requires the OT type to implement `compose`. +// It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`. // @param [callback] called after operation submitted // // @fires before op, op @@ -820,8 +819,7 @@ Doc.prototype.submitOp = function(component, options, callback) { // @param options.skipNoop should processing be skipped entirely, if the generated operation is a no-op. // @param options.undoable should the operation be undoable // @param options.fixUp If true, this operation is meant to fix the current invalid state of the snapshot. -// It also updates UndoManagers in such a way that undo/redo will "skip" over the current state of the snapshot. -// This feature requires the OT type to implement `compose`. +// It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`. // @param options.diffHint a hint passed into diff/diffX // @param [callback] called after operation submitted diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index d874d9762..372caab1c 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -21,9 +21,10 @@ function setLast(list, item) { list[lastIndex] = item; } -function Item(op, doc) { +function Op(op, doc) { this.op = op; this.doc = doc; + this.needsUndoOp = true; } // Manages an undo/redo stack for all operations from the specified `source`. @@ -51,10 +52,6 @@ function UndoManager(connection, options) { // The timestamp of the previous reversible operation. Used to determine if // the next reversible operation can be composed on the undoStack. this._previousUndoableOperationTime = -Infinity; - - // The type of operation that is currently in progress. - // It depends on the `op` event being triggered synchronously when submitting an operation or snapshot. - this._operationInProgress = null; } UndoManager.prototype.destroy = function() { @@ -97,12 +94,8 @@ UndoManager.prototype.undo = function(options, callback) { return; } - this._operationInProgress = 'undo'; - var op = this._undoStack.pop(); - var submitOptions = { - source: options && options.source, - undoable: true - }; + var op = getLast(this._undoStack); + var submitOptions = { source: options && options.source }; op.doc._submit(op, submitOptions, callback); }; @@ -127,12 +120,8 @@ UndoManager.prototype.redo = function(options, callback) { return; } - this._operationInProgress = 'redo'; - var op = this._redoStack.pop(); - var submitOptions = { - source: options && options.source, - undoable: true - }; + var op = getLast(this._redoStack); + var submitOptions = { source: options && options.source }; op.doc._submit(op, submitOptions, callback); }; @@ -163,19 +152,19 @@ UndoManager.prototype.onDocDelete = function(doc) { }; UndoManager.prototype.onDocOp = function(doc, op, undoOp, source, undoable, fixUp) { - if (this._operationInProgress === 'undo') { - this._updateStacksUndo(doc, op, undoOp); - this._operationInProgress = null; + if (this.canUndo() && getLast(this._undoStack) === op) { + this._undoStack.pop(); + this._updateStacksUndo(doc, op.op, undoOp.op); - } else if (this._operationInProgress === 'redo') { - this._updateStacksRedo(doc, op, undoOp); - this._operationInProgress = null; + } else if (this.canRedo() && getLast(this._redoStack) === op) { + this._redoStack.pop(); + this._updateStacksRedo(doc, op.op, undoOp.op); } else if (!fixUp && undoable && (this._source == null || this._source === source)) { - this._updateStacksUndoable(doc, op, undoOp); + this._updateStacksUndoable(doc, op.op, undoOp.op); } else { - this._updateStacksFixed(doc, op, undoOp, fixUp); + this._updateStacksFixed(doc, op.op, undoOp && undoOp.op, fixUp); } }; @@ -187,24 +176,24 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { getLast(this._undoStack).doc !== doc || now - this._previousUndoableOperationTime > this._composeTimeout ) { - this._undoStack.push(new Item(undoOp, doc)); + this._undoStack.push(new Op(undoOp, doc)); } else if (doc.type.composeSimilar) { var lastOp = getLast(this._undoStack); var composedOp = doc.type.composeSimilar(undoOp, lastOp.op); if (composedOp != null) { - setLast(this._undoStack, new Item(composedOp, doc)); + setLast(this._undoStack, new Op(composedOp, doc)); } else { - this._undoStack.push(new Item(undoOp, doc)); + this._undoStack.push(new Op(undoOp, doc)); } } else if (doc.type.compose) { var lastOp = getLast(this._undoStack); var composedOp = doc.type.compose(undoOp, lastOp.op); - setLast(this._undoStack, new Item(composedOp, doc)); + setLast(this._undoStack, new Op(composedOp, doc)); } else { - this._undoStack.push(new Item(undoOp, doc)); + this._undoStack.push(new Op(undoOp, doc)); } this._redoStack.length = 0; @@ -224,7 +213,7 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { /* istanbul ignore else */ if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { - this._redoStack.push(new Item(undoOp, doc)); + this._redoStack.push(new Op(undoOp, doc)); } this._previousUndoableOperationTime = -Infinity; }; @@ -232,19 +221,19 @@ UndoManager.prototype._updateStacksUndo = function(doc, op, undoOp) { UndoManager.prototype._updateStacksRedo = function(doc, op, undoOp) { /* istanbul ignore else */ if (!doc.type.isNoop || !doc.type.isNoop(undoOp)) { - this._undoStack.push(new Item(undoOp, doc)); + this._undoStack.push(new Op(undoOp, doc)); } this._previousUndoableOperationTime = -Infinity; }; UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { - if (fixUp && undoOp && doc.type.compose) { + if (fixUp && undoOp != null && doc.type.compose) { var lastUndoIndex = findLastIndex(this._undoStack, doc); if (lastUndoIndex >= 0) { var lastOp = this._undoStack[lastUndoIndex]; var composedOp = doc.type.compose(undoOp, lastOp.op); if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { - this._undoStack[lastUndoIndex] = new Item(composedOp, doc); + this._undoStack[lastUndoIndex] = new Op(composedOp, doc); } else { this._undoStack.splice(lastUndoIndex, 1); } @@ -255,7 +244,7 @@ UndoManager.prototype._updateStacksFixed = function(doc, op, undoOp, fixUp) { var lastOp = this._redoStack[lastRedoIndex]; var composedOp = doc.type.compose(undoOp, lastOp.op); if (!doc.type.isNoop || !doc.type.isNoop(composedOp)) { - this._redoStack[lastRedoIndex] = new Item(composedOp, doc); + this._redoStack[lastRedoIndex] = new Op(composedOp, doc); } else { this._redoStack.splice(lastRedoIndex, 1); } @@ -294,7 +283,7 @@ UndoManager.prototype._transformStack = function(stack, doc, op) { } if (!isNoop || !isNoop(transformedStackOp)) { - newStack[newStackIndex++] = new Item(transformedStackOp, doc); + newStack[newStackIndex++] = new Op(transformedStackOp, doc); } op = transformedOp; diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 66f632600..075df3df4 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -652,7 +652,7 @@ describe('client undo/redo', function() { }.bind(this)); }); - it('transforms the stacks by a local FIXED operation', function() { + it('transforms the stacks by a local operation', function() { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); @@ -677,7 +677,7 @@ describe('client undo/redo', function() { expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); }); - it('transforms the stacks by a local FIXED operation and removes no-ops', function() { + it('transforms the stacks by a local operation and removes no-ops', function() { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); @@ -700,6 +700,102 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(false); }); + it('transforms stacks by an undoable op', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true, source: '1' }); + undoManager.undo(); + undoManager.undo(); + + // The source does not match, so undoManager transforms its stacks rather than pushing this op on its undo stack. + this.doc.submitOp([ otRichText.Action.createInsertText('ABC') ], { undoable: true }); + + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); + }); + + it('transforms stacks by an undo op', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); + var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true, source: '1' }); + undoManager.undo(); + undoManager.undo(); + + // These 2 ops cancel each other out, so the undoManager's stacks remain unaffected, + // even though they are transformed against those ops. + // The second op has `source: '2'`, so it is inverted and added to the undo stack of undoManager2. + this.doc.submitOp([ otRichText.Action.createInsertText('ABC') ], { undoable: true }); + this.doc.submitOp([ otRichText.Action.createDelete(3) ], { undoable: true, source: '2' }); + // This inserts ABC at position 0 and the undoManager's stacks are transformed accordingly, ready for testing. + undoManager2.undo(); + + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); + }); + + it('transforms stacks by a redo op', function() { + var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); + var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + this.doc.create([], otRichText.type.uri); + this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('2') ], { undoable: true, source: '1' }); + this.doc.submitOp([ otRichText.Action.createInsertText('1') ], { undoable: true, source: '1' }); + undoManager.undo(); + undoManager.undo(); + + // submitOp and undo cancel each other out, so the undoManager's stacks remain unaffected, + // even though they are transformed against those ops. + // The second op has `source: '2'`, so it is inverted and added to the undo stack of undoManager2. + this.doc.submitOp([ otRichText.Action.createInsertText('ABC') ], { undoable: true, source: '2' }); + undoManager2.undo(); + // This inserts ABC at position 0 and the undoManager's stacks are transformed accordingly, ready for testing. + undoManager2.redo(); + + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC4') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC34') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC234') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ABC1234') ]); + }); + it('transforms the stacks using transform', function() { var undoManager = this.connection.undoManager({ composeTimeout: -1 }); this.doc.create(0, invertibleType.type.uri); @@ -979,6 +1075,124 @@ describe('client undo/redo', function() { this.redo().assert('bcd'); this.redo().assert('abcd'); }); + + it('fixes up ops if both fixUp and undoable are true', function() { + this.submitOp('!', { undoable: true, fixUp: true }).assert('!cd'); + this.undo().assert('d'); + this.undo().assert(''); + this.redo().assert('d'); + this.redo().assert('!cd'); + this.redo().assert('bcd'); + this.redo().assert('abcd'); + }); + }); + + it('filters undo/redo ops by source', function() { + var undoManager1 = this.connection.undoManager({ composeTimeout: -1, source: '1' }); + var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + + this.doc.create({ test: 5 }); + expect(this.doc.data.test).to.equal(5); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 2 }], { undoable: true }); + expect(this.doc.data.test).to.equal(7); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 2 }], { undoable: true, source: '3' }); + expect(this.doc.data.test).to.equal(9); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 7 }], { undoable: true, source: '1' }); + expect(this.doc.data.test).to.equal(16); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 7 }], { undoable: true, source: '1' }); + expect(this.doc.data.test).to.equal(23); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 13 }], { undoable: true, source: '2' }); + expect(this.doc.data.test).to.equal(36); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(true); + expect(undoManager2.canRedo()).to.equal(false); + + this.doc.submitOp([{ p: [ 'test' ], na: 13 }], { undoable: true, source: '2' }); + expect(this.doc.data.test).to.equal(49); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(true); + expect(undoManager2.canRedo()).to.equal(false); + + undoManager1.undo(); + expect(this.doc.data.test).to.equal(42); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(true); + expect(undoManager2.canUndo()).to.equal(true); + expect(undoManager2.canRedo()).to.equal(false); + + undoManager2.undo(); + expect(this.doc.data.test).to.equal(29); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(true); + expect(undoManager2.canUndo()).to.equal(true); + expect(undoManager2.canRedo()).to.equal(true); + + undoManager1.undo(); + expect(this.doc.data.test).to.equal(22); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(true); + expect(undoManager2.canUndo()).to.equal(true); + expect(undoManager2.canRedo()).to.equal(true); + + undoManager2.undo(); + expect(this.doc.data.test).to.equal(9); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(true); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(true); + }); + + it('cannot undo/redo an undo/redo operation', function() { + var undoManager1 = this.connection.undoManager(); + this.doc.create({ test: 5 }); + this.doc.submitOp([{ p: [ 'test' ], na: 2 }], { undoable: true }); + var undoManager2 = this.connection.undoManager(); + expect(this.doc.data.test).to.equal(7); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + undoManager1.undo(); + expect(this.doc.data.test).to.equal(5); + expect(undoManager1.canUndo()).to.equal(false); + expect(undoManager1.canRedo()).to.equal(true); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); + + undoManager1.redo(); + expect(this.doc.data.test).to.equal(7); + expect(undoManager1.canUndo()).to.equal(true); + expect(undoManager1.canRedo()).to.equal(false); + expect(undoManager2.canUndo()).to.equal(false); + expect(undoManager2.canRedo()).to.equal(false); }); it('destroys UndoManager', function() { From eb3cea2558270775e85b1e74d5297ba981430fb8 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 23:38:16 +0200 Subject: [PATCH 24/27] Rename a method and option for undo manager - connection.undoManager() -> connection.createUndoMananger() - composeTimeout -> composeInterval --- README.md | 6 +- lib/client/connection.js | 2 +- lib/client/undoManager.js | 4 +- test/client/undo-redo.js | 138 +++++++++++++++++++------------------- 4 files changed, 75 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 4b1d4095e..1d8178d9f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ var socket = new WebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); ``` -The native Websocket object that you feed to ShareDB's `Connection` constructor **does not** handle reconnections. +The native Websocket object that you feed to ShareDB's `Connection` constructor **does not** handle reconnections. The easiest way is to give it a WebSocket object that does reconnect. There are plenty of example on the web. The most important thing is that the custom reconnecting websocket, must have the same API as the native rfc6455 version. @@ -229,11 +229,11 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance. * `options.*` All other options are passed through to the database adapter. -`connection.undoManager(options)` creates a new `UndoManager`. +`connection.createUndoManager(options)` creates a new `UndoManager`. * `options.source` if specified, only the operations from that `source` will be undo-able. If `null` or `undefined`, the `source` filter is disabled. * `options.limit` the max number of operations to keep on the undo stack. -* `options.composeTimeout` the max time difference between operations in milliseconds, which still allows the operations to be composed on the undoStack. +* `options.composeInterval` the max time difference between operations in milliseconds, which still allows the operations to be composed on the undo stack. ### Class: `ShareDB.Doc` diff --git a/lib/client/connection.js b/lib/client/connection.js index fe99a0048..cbef375aa 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -589,7 +589,7 @@ Connection.prototype._firstQuery = function(fn) { } }; -Connection.prototype.undoManager = function(options) { +Connection.prototype.createUndoManager = function(options) { var undoManager = new UndoManager(this, options); this.undoManagers.push(undoManager); return undoManager; diff --git a/lib/client/undoManager.js b/lib/client/undoManager.js index 372caab1c..2c21312a4 100644 --- a/lib/client/undoManager.js +++ b/lib/client/undoManager.js @@ -41,7 +41,7 @@ function UndoManager(connection, options) { // The max time difference between operations in milliseconds, // which still allows the operations to be composed on the undoStack. - this._composeTimeout = options && typeof options.composeTimeout === 'number' ? options.composeTimeout : 1000; + this._composeInterval = options && typeof options.composeInterval === 'number' ? options.composeInterval : 1000; // Undo stack for local operations. this._undoStack = []; @@ -174,7 +174,7 @@ UndoManager.prototype._updateStacksUndoable = function(doc, op, undoOp) { if ( this._undoStack.length === 0 || getLast(this._undoStack).doc !== doc || - now - this._previousUndoableOperationTime > this._composeTimeout + now - this._previousUndoableOperationTime > this._composeInterval ) { this._undoStack.push(new Op(undoOp, doc)); diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 075df3df4..90a53d2a3 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -36,7 +36,7 @@ describe('client undo/redo', function() { }); it('submits a non-undoable operation', function(allDone) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ]), @@ -51,7 +51,7 @@ describe('client undo/redo', function() { }); it('receives a remote operation', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc2.preventCompose = true; this.doc.on('op', function() { expect(this.doc.version).to.equal(2); @@ -67,7 +67,7 @@ describe('client undo/redo', function() { }); it('submits an undoable operation', function(allDone) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -82,7 +82,7 @@ describe('client undo/redo', function() { }); it('undoes an operation', function(allDone) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -98,7 +98,7 @@ describe('client undo/redo', function() { }); it('redoes an operation', function(allDone) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -115,7 +115,7 @@ describe('client undo/redo', function() { }); it('performs a series of undo and redo operations', function(allDone) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -136,7 +136,7 @@ describe('client undo/redo', function() { }); it('performs a series of undo and redo operations synchronously', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }), this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }), expect(this.doc.data).to.eql({ test: 7 }); @@ -157,7 +157,7 @@ describe('client undo/redo', function() { }); it('undoes one of two operations', function(allDone) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -174,7 +174,7 @@ describe('client undo/redo', function() { }); it('undoes two of two operations', function(allDone) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -192,7 +192,7 @@ describe('client undo/redo', function() { }); it('redoes one of two operations', function(allDone) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -211,7 +211,7 @@ describe('client undo/redo', function() { }); it('redoes two of two operations', function(allDone) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); async.series([ this.doc.create.bind(this.doc, { test: 5 }), this.doc.submitOp.bind(this.doc, [ { p: [ 'test' ], na: 2 } ], { undoable: true }), @@ -231,25 +231,25 @@ describe('client undo/redo', function() { }); it('calls undo, when canUndo is false', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); expect(undoManager.canUndo()).to.equal(false); undoManager.undo(done); }); it('calls undo, when canUndo is false - no callback', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); expect(undoManager.canUndo()).to.equal(false); undoManager.undo(); }); it('calls redo, when canRedo is false', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); expect(undoManager.canRedo()).to.equal(false); undoManager.redo(done); }); it('calls redo, when canRedo is false - no callback', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); expect(undoManager.canRedo()).to.equal(false); undoManager.redo(); }); @@ -281,7 +281,7 @@ describe('client undo/redo', function() { }); it('preserves source on undo', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.on('op', function(op, source) { @@ -292,7 +292,7 @@ describe('client undo/redo', function() { }); it('preserves source on redo', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); undoManager.undo(); @@ -316,7 +316,7 @@ describe('client undo/redo', function() { }); it('composes undoable operations within time limit', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); setTimeout(function() { @@ -331,7 +331,7 @@ describe('client undo/redo', function() { }); it('composes undoable operations correctly', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ a: 1, b: 2 }); this.doc.submitOp([ { p: [ 'a' ], od: 1 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'b' ], od: 2 } ], { undoable: true }); @@ -352,7 +352,7 @@ describe('client undo/redo', function() { }); it('does not compose undoable operations outside time limit', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); setTimeout(function () { @@ -369,8 +369,8 @@ describe('client undo/redo', function() { this.clock.runAll(); }); - it('does not compose undoable operations, if composeTimeout < 0', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + it('does not compose undoable operations, if composeInterval < 0', function() { + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 3 } ], { undoable: true }); @@ -384,7 +384,7 @@ describe('client undo/redo', function() { }); it('does not compose undoable operations, if type does not support compose nor composeSimilar', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.type.uri); this.doc.submitOp(2, { undoable: true }); expect(this.doc.data).to.equal(7); @@ -403,7 +403,7 @@ describe('client undo/redo', function() { }); it('uses applyAndInvert, if available', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('two') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('two') ]); @@ -456,7 +456,7 @@ describe('client undo/redo', function() { }); it('fails to submit with fixUp, if type is not invertible', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create('two', otText.type.uri); this.doc.on('error', done); this.doc.submitOp([ 'one' ], { fixUp: true }, function(err) { @@ -466,7 +466,7 @@ describe('client undo/redo', function() { }); it('composes similar operations', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('one') @@ -486,7 +486,7 @@ describe('client undo/redo', function() { }); it('does not compose dissimilar operations', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create([ otRichText.Action.createInsertText(' ') ], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createRetain(1), otRichText.Action.createInsertText('two') ], { undoable: true }); @@ -509,7 +509,7 @@ describe('client undo/redo', function() { }); it('does not add no-ops to the undo stack on undoable operation', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); var opCalled = false; this.doc.create([ otRichText.Action.createInsertText('test', [ 'key', 'value' ]) ], otRichText.type.uri); this.doc.on('op', function(op, source) { @@ -524,7 +524,7 @@ describe('client undo/redo', function() { }); it('limits the size of the undo stack', function() { - var undoManager = this.connection.undoManager({ limit: 2, composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ limit: 2, composeInterval: -1 }); this.doc.create({ test: 5 }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); @@ -540,7 +540,7 @@ describe('client undo/redo', function() { }); it('does not compose the next operation after undo', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed @@ -562,7 +562,7 @@ describe('client undo/redo', function() { }); it('does not compose the next operation after undo and redo', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.clock.tick(1001); this.doc.submitOp([ { p: [ 'test' ], na: 2 } ], { undoable: true }); // not composed @@ -589,7 +589,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks by remote operations', function(done) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc2.subscribe(); this.doc.subscribe(); this.doc.create([], otRichText.type.uri); @@ -622,7 +622,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks by remote operations and removes no-ops', function(done) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc2.subscribe(); this.doc.subscribe(); this.doc.create([], otRichText.type.uri); @@ -653,7 +653,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks by a local operation', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); @@ -678,7 +678,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks by a local operation and removes no-ops', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true }); @@ -701,7 +701,7 @@ describe('client undo/redo', function() { }); it('transforms stacks by an undoable op', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1, source: '1' }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); @@ -729,8 +729,8 @@ describe('client undo/redo', function() { }); it('transforms stacks by an undo op', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); - var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1, source: '1' }); + var undoManager2 = this.connection.createUndoManager({ composeInterval: -1, source: '2' }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); @@ -763,8 +763,8 @@ describe('client undo/redo', function() { }); it('transforms stacks by a redo op', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1, source: '1' }); - var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1, source: '1' }); + var undoManager2 = this.connection.createUndoManager({ composeInterval: -1, source: '2' }); this.doc.create([], otRichText.type.uri); this.doc.submitOp([ otRichText.Action.createInsertText('4') ], { undoable: true, source: '1' }); this.doc.submitOp([ otRichText.Action.createInsertText('3') ], { undoable: true, source: '1' }); @@ -797,7 +797,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks using transform', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create(0, invertibleType.type.uri); this.doc.submitOp(1, { undoable: true }); this.doc.submitOp(10, { undoable: true }); @@ -822,7 +822,7 @@ describe('client undo/redo', function() { }); it('transforms the stacks using transformX', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create(0, invertibleType.typeWithTransformX.uri); this.doc.submitOp(1, { undoable: true }); this.doc.submitOp(10, { undoable: true }); @@ -902,7 +902,7 @@ describe('client undo/redo', function() { describe('fixup operations', function() { beforeEach(function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.assert = function(text) { var expected = text ? [ otRichText.Action.createInsertText(text) ] : []; @@ -944,7 +944,7 @@ describe('client undo/redo', function() { }); it('does not fix up anything', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); expect(undoManager.canUndo()).to.equal(false); expect(undoManager.canRedo()).to.equal(false); this.submitOp('!', { fixUp: true }).assert('!cd'); @@ -1088,8 +1088,8 @@ describe('client undo/redo', function() { }); it('filters undo/redo ops by source', function() { - var undoManager1 = this.connection.undoManager({ composeTimeout: -1, source: '1' }); - var undoManager2 = this.connection.undoManager({ composeTimeout: -1, source: '2' }); + var undoManager1 = this.connection.createUndoManager({ composeInterval: -1, source: '1' }); + var undoManager2 = this.connection.createUndoManager({ composeInterval: -1, source: '2' }); this.doc.create({ test: 5 }); expect(this.doc.data.test).to.equal(5); @@ -1170,10 +1170,10 @@ describe('client undo/redo', function() { }); it('cannot undo/redo an undo/redo operation', function() { - var undoManager1 = this.connection.undoManager(); + var undoManager1 = this.connection.createUndoManager(); this.doc.create({ test: 5 }); this.doc.submitOp([{ p: [ 'test' ], na: 2 }], { undoable: true }); - var undoManager2 = this.connection.undoManager(); + var undoManager2 = this.connection.createUndoManager(); expect(this.doc.data.test).to.equal(7); expect(undoManager1.canUndo()).to.equal(true); expect(undoManager1.canRedo()).to.equal(false); @@ -1196,7 +1196,7 @@ describe('client undo/redo', function() { }); it('destroys UndoManager', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create({ test: 5 }); @@ -1225,7 +1225,7 @@ describe('client undo/redo', function() { describe('UndoManager.clear', function() { it('clears the stacks', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create({ test: 5 }); @@ -1251,7 +1251,7 @@ describe('client undo/redo', function() { }); it('clears the stacks for a specific document', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create({ test: 5 }); @@ -1291,7 +1291,7 @@ describe('client undo/redo', function() { it('clears the stacks for a specific document on del', function() { // NOTE we don't support undo/redo on del/create at the moment. // See undoManager.js for more details. - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create({ test: 5 }); @@ -1319,7 +1319,7 @@ describe('client undo/redo', function() { next(); }); - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create([], otRichText.type.uri); @@ -1366,7 +1366,7 @@ describe('client undo/redo', function() { }); it('clears the stacks for a specific document on doc destroy', function(done) { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); var doc1 = this.connection.get('dogs', 'fido'); var doc2 = this.connection.get('dogs', 'toby'); doc1.create({ test: 5 }); @@ -1432,7 +1432,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot with source (no callback)', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); @@ -1446,7 +1446,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot with source (with callback)', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); var opEmitted = false; this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); @@ -1464,7 +1464,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot without source (no callback)', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); @@ -1478,7 +1478,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot without source (with callback)', function(done) { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); var opEmitted = false; this.doc.on('op', function(op, source) { expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); @@ -1496,7 +1496,7 @@ describe('client undo/redo', function() { }); it('submits snapshots and supports undo and redo', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); @@ -1520,7 +1520,7 @@ describe('client undo/redo', function() { }); it('submits snapshots and composes operations', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); @@ -1553,7 +1553,7 @@ describe('client undo/redo', function() { }); it('submits undoable and fixed operations', function() { - var undoManager = this.connection.undoManager({ composeTimeout: -1 }); + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); this.doc.create([], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('a') ], { undoable: true }); this.doc.submitSnapshot([ otRichText.Action.createInsertText('ab') ], { undoable: true }); @@ -1589,7 +1589,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot without a diffHint', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); var opCalled = 0; this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true }); @@ -1613,7 +1613,7 @@ describe('client undo/redo', function() { }); it('submits a snapshot with a diffHint', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); var opCalled = 0; this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true, diffHint: 2 }); @@ -1661,7 +1661,7 @@ describe('client undo/redo', function() { describe('with diff', function () { it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiff.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); @@ -1669,7 +1669,7 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiff.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); @@ -1682,7 +1682,7 @@ describe('client undo/redo', function() { describe('with diffX', function () { it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiffX.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); @@ -1690,7 +1690,7 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiffX.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); @@ -1703,7 +1703,7 @@ describe('client undo/redo', function() { describe('with diff and diffX', function () { it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); this.doc.submitSnapshot(7); expect(this.doc.data).to.equal(7); @@ -1711,7 +1711,7 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(false); }); it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.undoManager(); + var undoManager = this.connection.createUndoManager(); this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); this.doc.submitSnapshot(7, { undoable: true }); expect(this.doc.data).to.equal(7); From 684725aa7d4d00cc73eb615d3f87c8e434fe6256 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 10:47:07 +0200 Subject: [PATCH 25/27] Update docs --- README.md | 63 ++++++++++++++++++++-------------------- lib/client/doc.js | 4 +-- test/client/submit.js | 5 ++-- test/client/undo-redo.js | 16 ++++++++++ 4 files changed, 51 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1d8178d9f..77bea5808 100644 --- a/README.md +++ b/README.md @@ -246,13 +246,6 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. -`doc.undoLimit` _(Number, read-write, default=100)_ -The max number of operations to keep on the undo stack. - -`doc.undoComposeTimeout` _(Number, read-write, default=1000)_ -The max time difference between operations in milliseconds, -which still allows "UNDOABLE" operations to be composed on the undo stack. - `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. @@ -273,11 +266,11 @@ 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'), function(op, source, operationType) {...})` -An operation is about to be applied to the data. Params are the same as for the `op` event below. +`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, operationType) {...})` -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. `operationType` is one of the following: `"UNDOABLE"` _(local operation that can be undone)_, `"FIXED"` _(local or remote operation that can't be undone nor redone)_, `"UNDO"` _(local undo operation that can be redone)_ and `"REDO"` _(local redo operation that can be undone)_. +`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('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. @@ -302,19 +295,17 @@ Apply operation to document and send it to the server. Call this after you've either fetched or subscribed to the document. * `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. * `options.skipNoop` Should processing be skipped entirely, if `op` is a no-op. Defaults to `false`. -* `options.undoable` Should it be possible to undo this operation, default=false. -* `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. -* `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. +* `options.undoable` Should it be possible to undo this operation. Defaults to `false`. +* `options.fixUp` If true, this operation is meant to fix the current invalid state of the snapshot. It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`. `doc.submitSnapshot(snapshot[, options][, function(err) {...}])` Diff the current and the provided snapshots to generate an operation, apply the operation to the document and send it to the server. `snapshot` structure depends on the document type. Call this after you've either fetched or subscribed to the document. * `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. -* `options.skipNoop` Should processing be skipped entirely, if the generated operation is a no-op. Defaults to `false`. -* `options.undoable` Should it be possible to undo this operation, default=false. -* `options.fixUpUndoStack` Determines how a non-undoable operation affects the undo stack. If `false` (default), the operation transforms the undo stack, otherwise it is inverted and composed into the last operation on the undo stack. -* `options.fixUpRedoStack` Determines how a non-undoable operation affects the redo stack. If `false` (default), the operation transforms the redo stack, otherwise it is inverted and composed into the last operation on the redo stack. +* `options.skipNoop` Should processing be skipped entirely, if `op` is a no-op. Defaults to `false`. +* `options.undoable` Should it be possible to undo this operation. Defaults to `false`. +* `options.fixUp` If true, this operation is meant to fix the current invalid state of the snapshot. It also updates UndoManagers accordingly. This feature requires the OT type to implement `compose`. * `options.diffHint` A hint passed into the `diff`/`diffX` functions defined by the document type. `doc.del([options][, function(err) {...}])` @@ -330,20 +321,6 @@ Invokes the given callback function after Note that `whenNothingPending` does NOT wait for pending `model.query()` calls. -`doc.canUndo()` -Return `true`, if there's an operation on the undo stack that can be undone, otherwise `false`. - -`doc.undo([options][, function(err) {...}])` -Undo a previously applied "UNDOABLE" or "REDO" operation. -* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. - -`doc.canRedo()` -Return `true`, if there's an operation on the redo stack that can be undone, otherwise `false`. - -`doc.redo([options][, function(err) {...}])` -Redo a previously applied "UNDO" operation. -* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. - ### Class: `ShareDB.Query` `query.ready` _(Boolean)_ @@ -381,6 +358,28 @@ after a sequence of diffs are handled. `query.on('extra', function() {...}))` (Only fires on subscription queries) `query.extra` changed. +### Class: `ShareDB.UndoManager` + +`undoManager.canUndo()` +Return `true`, if there's an operation on the undo stack that can be undone, otherwise `false`. + +`undoManager.undo([options][, function(err) {...}])` +Undo a previously applied undoable or redo operation. +* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. + +`undoManager.canRedo()` +Return `true`, if there's an operation on the redo stack that can be undone, otherwise `false`. + +`undoManager.redo([options][, function(err) {...}])` +Redo a previously applied undo operation. +* `options.source` Argument passed to the `'op'` event locally. This is not sent to the server or other clients. Defaults to `true`. + +`undoManager.clear(doc)` +Remove operations from the undo and redo stacks. +* `doc` if specified, only the operations on that doc are removed, otherwise all operations are removed. + +`undoManager.destroy()` +Remove all operations from the undo and redo stacks, and stop recording new operations. ## Error codes diff --git a/lib/client/doc.js b/lib/client/doc.js index a79a956d9..3848ca577 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -680,9 +680,7 @@ Doc.prototype._submit = function(op, options, callback) { if (callback) return callback(err); return this.emit('error', err); } - var undoable = options && options.undoable; - var fixUp = options && options.fixUp; - var needsUndoOp = undoable || fixUp || op.needsUndoOp; + var needsUndoOp = options.undoable || options.fixUp || op.needsUndoOp; if (needsUndoOp && !this.type.invert && !this.type.applyAndInvert) { var err = new ShareDBError(4025, 'Cannot submit op. OT type does not support invert not applyAndInvert. ' + this.collection + '.' + this.id); if (callback) return callback(err); diff --git a/test/client/submit.js b/test/client/submit.js index 69a98aabd..95bb6f560 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -598,7 +598,7 @@ describe('client submit', function() { }); }); - it('transforming pending op by server delete emits error', function(done) { + it('transforming pending op by server delete returns error', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -626,7 +626,7 @@ describe('client submit', function() { }); }); - it('transforming pending op by server create emits error', function(done) { + it('transforming pending op by server create returns error', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -645,6 +645,7 @@ describe('client submit', function() { }); doc.create({age: 9}, function(err) { expect(err).ok(); + expect(err.code).to.equal(4018); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); calledBack = true; diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 90a53d2a3..3aff7b0b1 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -1223,6 +1223,22 @@ describe('client undo/redo', function() { expect(undoManager.canRedo()).to.equal(false); }); + it('destroys UndoManager twice', function() { + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); + this.doc.create({ test: 5 }); + this.doc.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + this.doc.submitOp([ { p: [ 'test' ], 'na': 2 } ], { undoable: true }); + undoManager.undo(); + expect(undoManager.canUndo()).to.equal(true); + expect(undoManager.canRedo()).to.equal(true); + undoManager.destroy(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + undoManager.destroy(); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); + describe('UndoManager.clear', function() { it('clears the stacks', function() { var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); From e0787b7cc941cf452b3ef87aae81dd0bb06c175c Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 10:55:08 +0200 Subject: [PATCH 26/27] Add more tests --- test/client/doc.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/client/doc.js b/test/client/doc.js index b44f52a2b..c789ad67b 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,7 +1,7 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -describe('client query subscribe', function() { +describe('client doc', function() { beforeEach(function() { this.backend = new Backend(); @@ -25,6 +25,39 @@ describe('client query subscribe', function() { expect(doc).not.equal(doc2); }); + it('calling doc.destroy on subscribed doc unregisters it (no callback)', function() { + var connection = this.connection; + var doc = connection.get('dogs', 'fido'); + expect(connection.getExisting('dogs', 'fido')).equal(doc); + + doc.subscribe(function(err) { + if (err) return done(err); + doc.destroy(); + doc.whenNothingPending(function() { + expect(connection.getExisting('dogs', 'fido')).equal(undefined); + + var doc2 = connection.get('dogs', 'fido'); + expect(doc).not.equal(doc2); + }); + }); + }); + + it('calling doc.destroy on subscribed doc unregisters it (with callback)', function() { + var connection = this.connection; + var doc = connection.get('dogs', 'fido'); + expect(connection.getExisting('dogs', 'fido')).equal(doc); + + doc.subscribe(function(err) { + if (err) return done(err); + doc.destroy(function() { + expect(connection.getExisting('dogs', 'fido')).equal(undefined); + + var doc2 = connection.get('dogs', 'fido'); + expect(doc).not.equal(doc2); + }); + }); + }); + it('getting then destroying then getting returns a new doc object', function() { var doc = this.connection.get('dogs', 'fido'); doc.destroy(); From a788f1e7ac9fe9f554738bd6f20071a547f3f3ba Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 11:56:35 +0200 Subject: [PATCH 27/27] Move some tests --- test/client/submit.js | 223 ++++++++++++++++ test/client/undo-redo.js | 534 ++++++++++++--------------------------- 2 files changed, 391 insertions(+), 366 deletions(-) diff --git a/test/client/submit.js b/test/client/submit.js index 95bb6f560..8ee279b2e 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -2,8 +2,10 @@ var async = require('async'); var expect = require('expect.js'); var types = require('../../lib/types'); var deserializedType = require('./deserialized-type'); +var otRichText = require('@teamwork/ot-rich-text'); types.register(deserializedType.type); types.register(deserializedType.type2); +types.register(otRichText.type); module.exports = function() { describe('client submit', function() { @@ -1056,6 +1058,227 @@ describe('client submit', function() { }); }); + it('does not skip processing when submitting a no-op by default', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + expect(doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitOp([]); + }); + + it('does not skip processing when submitting an identical snapshot by default', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + expect(doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('test') ]); + }); + + it('skips processing when submitting a no-op (no callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitOp([], { skipNoop: true }); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + + it('skips processing when submitting a no-op (with callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitOp([], { skipNoop: true }, done); + }); + + it('skips processing when submitting an identical snapshot (no callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); + done(); + }); + + it('skips processing when submitting an identical snapshot (with callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function() { + done(new Error('Should not emit `op`')); + }); + doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }, done); + }); + + it('submits a snapshot when document is not created (no callback, no options)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + doc.submitSnapshot(7); + }); + + it('submits a snapshot when document is not created (no callback, with options)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + doc.submitSnapshot(7, { source: 'test' }); + }); + + it('submits a snapshot when document is not created (with callback, no options)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', done); + doc.submitSnapshot(7, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + }); + + it('submits a snapshot when document is not created (with callback, with options)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', done); + doc.submitSnapshot(7, { source: 'test' }, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4015); + done(); + }); + }); + + it('submits a snapshot with source (no callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal('test'); + done(); + }); + doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }); + }); + + it('submits a snapshot with source (with callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var opEmitted = false; + doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal('test'); + opEmitted = true; + }); + doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }, function(error) { + expect(opEmitted).to.equal(true); + done(error); + }); + }); + + it('submits a snapshot without source (no callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal(true); + done(); + }); + doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); + }); + + it('submits a snapshot without source (with callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var opEmitted = false; + doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + expect(source).to.equal(true); + opEmitted = true; + }); + doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], function(error) { + expect(opEmitted).to.equal(true); + done(error); + }); + }); + + it('submits a snapshot and syncs it', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc2.on('create', function() { + doc2.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); + }); + doc.on('op', function(op, source) { + expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); + expect(source).to.equal(false); + expect(doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); + done(); + }); + doc.subscribe(function(err) { + if (err) return done(err); + doc2.subscribe(function(err) { + if (err) return done(err); + doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); + }); + }); + }); + + it('submits a snapshot (no diff, no diffX, no callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4024); + done(); + }); + doc.create({ test: 5 }); + doc.submitSnapshot({ test: 7 }); + }); + + it('submits a snapshot (no diff, no diffX, with callback)', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.on('error', done); + doc.create({ test: 5 }); + doc.submitSnapshot({ test: 7 }, function(error) { + expect(error).to.be.an(Error); + expect(error.code).to.equal(4024); + done(); + }); + }); + + it('submits a snapshot without a diffHint', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + doc.on('op', function(op) { + expect(doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createInsertText('a') ]); + done(); + }); + doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ]); + }); + + it('submits a snapshot with a diffHint', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + doc.on('op', function(op) { + expect(doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createInsertText('a') ]); + done(); + }); + doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { diffHint: 2 }); + }); + describe('type.deserialize', function() { it('can create a new doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); diff --git a/test/client/undo-redo.js b/test/client/undo-redo.js index 3aff7b0b1..fe9681a54 100644 --- a/test/client/undo-redo.js +++ b/test/client/undo-redo.js @@ -846,60 +846,6 @@ describe('client undo/redo', function() { expect(this.doc.data).to.equal(11111); }); - it('does not skip processing when submitting a no-op by default', function(done) { - this.doc.on('op', function() { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); - done(); - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitOp([]); - }); - - it('does not skip processing when submitting an identical snapshot by default', function(done) { - this.doc.on('op', function() { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); - done(); - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ]); - }); - - it('skips processing when submitting a no-op (no callback)', function(done) { - this.doc.on('op', function() { - done(new Error('Should not emit `op`')); - }); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitOp([], { skipNoop: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); - done(); - }); - - it('skips processing when submitting a no-op (with callback)', function(done) { - this.doc.on('op', function() { - done(new Error('Should not emit `op`')); - }); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitOp([], { skipNoop: true }, done); - }); - - it('skips processing when submitting an identical snapshot (no callback)', function(done) { - this.doc.on('op', function() { - done(new Error('Should not emit `op`')); - }); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('test') ]); - done(); - }); - - it('skips processing when submitting an identical snapshot (with callback)', function(done) { - this.doc.on('op', function() { - done(new Error('Should not emit `op`')); - }); - this.doc.create([ otRichText.Action.createInsertText('test') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('test') ], { skipNoop: true }, done); - }); - describe('fixup operations', function() { beforeEach(function() { var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); @@ -1409,333 +1355,189 @@ describe('client undo/redo', function() { }); }); - describe('submitSnapshot', function() { - describe('basic tests', function() { - it('submits a snapshot when document is not created (no callback, no options)', function(done) { - this.doc.on('error', function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4015); - done(); - }); - this.doc.submitSnapshot(7); - }); - - it('submits a snapshot when document is not created (no callback, with options)', function(done) { - this.doc.on('error', function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4015); - done(); - }); - this.doc.submitSnapshot(7, { source: 'test' }); - }); - - it('submits a snapshot when document is not created (with callback, no options)', function(done) { - this.doc.on('error', done); - this.doc.submitSnapshot(7, function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4015); - done(); - }); - }); - - it('submits a snapshot when document is not created (with callback, with options)', function(done) { - this.doc.on('error', done); - this.doc.submitSnapshot(7, { source: 'test' }, function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4015); - done(); - }); - }); - - it('submits a snapshot with source (no callback)', function(done) { - var undoManager = this.connection.createUndoManager(); - this.doc.on('op', function(op, source) { - expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - expect(source).to.equal('test'); - done(); - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }); - }); - - it('submits a snapshot with source (with callback)', function(done) { - var undoManager = this.connection.createUndoManager(); - var opEmitted = false; - this.doc.on('op', function(op, source) { - expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(source).to.equal('test'); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - opEmitted = true; - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { source: 'test' }, function(error) { - expect(opEmitted).to.equal(true); - done(error); - }); - }); - - it('submits a snapshot without source (no callback)', function(done) { - var undoManager = this.connection.createUndoManager(); - this.doc.on('op', function(op, source) { - expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - expect(source).to.equal(true); - done(); - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); - }); + it('submits snapshots and supports undo and redo', function() { + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); + this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + expect(undoManager.canRedo()).to.equal(false); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(undoManager.canUndo()).to.equal(false); + }); - it('submits a snapshot without source (with callback)', function(done) { - var undoManager = this.connection.createUndoManager(); - var opEmitted = false; - this.doc.on('op', function(op, source) { - expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - expect(source).to.equal(true); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - opEmitted = true; - }.bind(this)); - this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], function(error) { - expect(opEmitted).to.equal(true); - done(error); - }); - }); + it('submits snapshots and composes operations', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(undoManager.canUndo()).to.equal(false); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); + expect(undoManager.canRedo()).to.equal(false); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); + expect(undoManager.canUndo()).to.equal(false); + }); - it('submits snapshots and supports undo and redo', function() { - var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); - this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(undoManager.canUndo()).to.equal(false); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - expect(undoManager.canRedo()).to.equal(false); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(undoManager.canUndo()).to.equal(false); - }); + it('submits undoable and non-undoable snapshots', function() { + var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); + this.doc.create([], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('a') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('ab') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcd') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcde') ], { undoable: true }); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { undoable: true }); + undoManager.undo(); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcd') ]); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc123d') ]); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); + undoManager.undo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('123') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123de') ]); + undoManager.redo(); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123def') ]); + }); - it('submits snapshots and composes operations', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create([ otRichText.Action.createInsertText('ghi') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('defghi') ], { undoable: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('defghi') ]); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdefghi') ], { undoable: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(undoManager.canUndo()).to.equal(false); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdefghi') ]); - expect(undoManager.canRedo()).to.equal(false); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ghi') ]); - expect(undoManager.canUndo()).to.equal(false); - }); + it('submits a snapshot without a diffHint', function() { + var undoManager = this.connection.createUndoManager(); + var opCalled = 0; + this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - it('submits a snapshot and syncs it', function(done) { - this.doc2.on('create', function() { - this.doc2.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ]); - }.bind(this)); - this.doc.on('op', function(op, source) { - expect(op).to.eql([ otRichText.Action.createInsertText('abc') ]); - expect(source).to.equal(false); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcdef') ]); - done(); - }.bind(this)); - this.doc2.subscribe(); - this.doc.subscribe(); - this.doc.create([ otRichText.Action.createInsertText('def') ], otRichText.type.uri); - }); + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); + expect(op).to.eql([ otRichText.Action.createDelete(1) ]); + opCalled++; + }.bind(this)); + undoManager.undo(); - it('submits undoable and fixed operations', function() { - var undoManager = this.connection.createUndoManager({ composeInterval: -1 }); - this.doc.create([], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('a') ], { undoable: true }); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('ab') ], { undoable: true }); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc') ], { undoable: true }); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcd') ], { undoable: true }); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcde') ], { undoable: true }); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abcdef') ], { undoable: true }); - undoManager.undo(); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abcd') ]); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('abc123d') ]); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); - undoManager.undo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('123') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('a123') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('ab123') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123d') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123de') ]); - undoManager.redo(); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('abc123def') ]); - }); + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createInsertText('a') ]); + opCalled++; + }.bind(this)); + undoManager.redo(); - it('submits a snapshot without a diffHint', function() { - var undoManager = this.connection.createUndoManager(); - var opCalled = 0; - this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - - this.doc.once('op', function(op) { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); - expect(op).to.eql([ otRichText.Action.createDelete(1) ]); - opCalled++; - }.bind(this)); - undoManager.undo(); + expect(opCalled).to.equal(2); + }); - this.doc.once('op', function(op) { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - expect(op).to.eql([ otRichText.Action.createInsertText('a') ]); - opCalled++; - }.bind(this)); - undoManager.redo(); + it('submits a snapshot with a diffHint', function() { + var undoManager = this.connection.createUndoManager(); + var opCalled = 0; + this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); + this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true, diffHint: 2 }); + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - expect(opCalled).to.equal(2); - }); + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); + expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createDelete(1) ]); + opCalled++; + }.bind(this)); + undoManager.undo(); - it('submits a snapshot with a diffHint', function() { - var undoManager = this.connection.createUndoManager(); - var opCalled = 0; - this.doc.create([ otRichText.Action.createInsertText('aaaa') ], otRichText.type.uri); - this.doc.submitSnapshot([ otRichText.Action.createInsertText('aaaaa') ], { undoable: true, diffHint: 2 }); - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - - this.doc.once('op', function(op) { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaa') ]); - expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createDelete(1) ]); - opCalled++; - }.bind(this)); - undoManager.undo(); + this.doc.once('op', function(op) { + expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); + expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createInsertText('a') ]); + opCalled++; + }.bind(this)); + undoManager.redo(); - this.doc.once('op', function(op) { - expect(this.doc.data).to.eql([ otRichText.Action.createInsertText('aaaaa') ]); - expect(op).to.eql([ otRichText.Action.createRetain(2), otRichText.Action.createInsertText('a') ]); - opCalled++; - }.bind(this)); - undoManager.redo(); + expect(opCalled).to.equal(2); + }); - expect(opCalled).to.equal(2); - }); - }); + it('submits a snapshot (with diff, non-undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiff.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); - describe('no diff nor diffX', function() { - it('submits a snapshot (no callback)', function(done) { - this.doc.on('error', function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4024); - done(); - }); - this.doc.create(5, invertibleType.type.uri); - this.doc.submitSnapshot(7); - }); + it('submits a snapshot (with diff, undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiff.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + undoManager.undo(); + expect(this.doc.data).to.equal(5); + undoManager.redo(); + expect(this.doc.data).to.equal(7); + }); - it('submits a snapshot (with callback)', function(done) { - this.doc.on('error', done); - this.doc.create(5, invertibleType.type.uri); - this.doc.submitSnapshot(7, function(error) { - expect(error).to.be.an(Error); - expect(error.code).to.equal(4024); - done(); - }); - }); - }); + it('submits a snapshot (with diffX, non-undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiffX.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); - describe('with diff', function () { - it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiff.uri); - this.doc.submitSnapshot(7); - expect(this.doc.data).to.equal(7); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - }); - it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiff.uri); - this.doc.submitSnapshot(7, { undoable: true }); - expect(this.doc.data).to.equal(7); - undoManager.undo(); - expect(this.doc.data).to.equal(5); - undoManager.redo(); - expect(this.doc.data).to.equal(7); - }); - }); + it('submits a snapshot (with diffX, undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiffX.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + undoManager.undo(); + expect(this.doc.data).to.equal(5); + undoManager.redo(); + expect(this.doc.data).to.equal(7); + }); - describe('with diffX', function () { - it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiffX.uri); - this.doc.submitSnapshot(7); - expect(this.doc.data).to.equal(7); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - }); - it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiffX.uri); - this.doc.submitSnapshot(7, { undoable: true }); - expect(this.doc.data).to.equal(7); - undoManager.undo(); - expect(this.doc.data).to.equal(5); - undoManager.redo(); - expect(this.doc.data).to.equal(7); - }); - }); + it('submits a snapshot (with diff and diffX, non-undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); + this.doc.submitSnapshot(7); + expect(this.doc.data).to.equal(7); + expect(undoManager.canUndo()).to.equal(false); + expect(undoManager.canRedo()).to.equal(false); + }); - describe('with diff and diffX', function () { - it('submits a snapshot (non-undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); - this.doc.submitSnapshot(7); - expect(this.doc.data).to.equal(7); - expect(undoManager.canUndo()).to.equal(false); - expect(undoManager.canRedo()).to.equal(false); - }); - it('submits a snapshot (undoable)', function() { - var undoManager = this.connection.createUndoManager(); - this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); - this.doc.submitSnapshot(7, { undoable: true }); - expect(this.doc.data).to.equal(7); - undoManager.undo(); - expect(this.doc.data).to.equal(5); - undoManager.redo(); - expect(this.doc.data).to.equal(7); - }); - }); + it('submits a snapshot (with diff and diffX, undoable)', function() { + var undoManager = this.connection.createUndoManager(); + this.doc.create(5, invertibleType.typeWithDiffAndDiffX.uri); + this.doc.submitSnapshot(7, { undoable: true }); + expect(this.doc.data).to.equal(7); + undoManager.undo(); + expect(this.doc.data).to.equal(5); + undoManager.redo(); + expect(this.doc.data).to.equal(7); }); });