jenkins-bot has submitted this change and it was merged.

Change subject: Rebase logic
......................................................................


Rebase logic

Change-Id: I2876abff50eb37a80b8c2407db64a6e39393aed1
---
M .jsduck/categories.json
M .jsduck/external.js
M build/modules.json
A src/dm/ve.dm.RebaseClient.js
A src/dm/ve.dm.RebaseServer.js
A tests/dm/ve.dm.RebaseServer.test.js
M tests/index.html
7 files changed, 589 insertions(+), 2 deletions(-)

Approvals:
  Jforrester: Looks good to me, but someone else must approve
  Esanders: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.jsduck/categories.json b/.jsduck/categories.json
index 08893c3..b65f717 100644
--- a/.jsduck/categories.json
+++ b/.jsduck/categories.json
@@ -64,7 +64,6 @@
                                        "ve.dm.*Selection",
                                        "ve.dm.Transaction",
                                        "ve.dm.TransactionBuilder",
-                                       "ve.dm.Change",
                                        "ve.dm.TransactionProcessor",
                                        "ve.dm.TransactionProcessor.*"
                                ]
@@ -99,6 +98,14 @@
                                        "ve.dm.TableMatrix",
                                        "ve.dm.TableMatrixCell",
                                        "ve.dm.TableNodeCellIterator"
+                               ]
+                       },
+                       {
+                               "name": "Rebasing",
+                               "classes": [
+                                       "ve.dm.Change",
+                                       "ve.dm.RebaseServer",
+                                       "ve.dm.RebaseClient"
                                ]
                        }
                ]
@@ -325,6 +332,7 @@
                                        "Boolean",
                                        "Date",
                                        "Function",
+                                       "Map",
                                        "Number",
                                        "Object",
                                        "RegExp",
diff --git a/.jsduck/external.js b/.jsduck/external.js
index 6d87218..026bad7 100644
--- a/.jsduck/external.js
+++ b/.jsduck/external.js
@@ -35,3 +35,7 @@
  * @class QUnit
  * @source <http://api.qunitjs.com/>
  */
+
+/**
+ * @class Map
+ */
diff --git a/build/modules.json b/build/modules.json
index a30e032..7f74139 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -609,7 +609,9 @@
        },
        "visualEditor.rebase": {
                "scripts": [
-                       "src/dm/ve.dm.Change.js"
+                       "src/dm/ve.dm.Change.js",
+                       "src/dm/ve.dm.RebaseServer.js",
+                       "src/dm/ve.dm.RebaseClient.js"
                ],
                "dependencies": [
                        "visualEditor.core.build"
@@ -644,6 +646,7 @@
                        "tests/dm/ve.dm.Transaction.test.js",
                        "tests/dm/ve.dm.TransactionBuilder.test.js",
                        "tests/dm/ve.dm.Change.test.js",
+                       "tests/dm/ve.dm.RebaseServer.test.js",
                        "tests/dm/ve.dm.TransactionProcessor.test.js",
                        "tests/dm/ve.dm.APIResultsQueue.test.js",
                        "tests/dm/ve.dm.Surface.test.js",
diff --git a/src/dm/ve.dm.RebaseClient.js b/src/dm/ve.dm.RebaseClient.js
new file mode 100644
index 0000000..58dd87b
--- /dev/null
+++ b/src/dm/ve.dm.RebaseClient.js
@@ -0,0 +1,165 @@
+/*!
+ * VisualEditor DataModel rebase client class.
+ *
+ * @copyright 2011-2016 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * DataModel rebase client
+ *
+ * @class
+ */
+ve.dm.RebaseClient = function VeDmRebaseClient() {
+       /**
+        * @property {number} author Author ID
+        */
+       this.author = null;
+
+       /**
+        * @property {number} commitLength Offset up to which we know we have 
no differences with the server
+        */
+       this.commitLength = 0;
+
+       /**
+        * @property {number} sentLength Offset up to which we have no unsent 
changes
+        */
+       this.sentLength = 0;
+
+       /**
+        * @property {number} backtrack Number of transactions backtracked 
(i.e. rejected) since the last send
+        */
+       this.backtrack = 0;
+
+};
+
+/* Inheritance */
+
+OO.initClass( ve.dm.RebaseClient );
+
+/* Abstract methods */
+
+/**
+ * @abstract
+ * @param {number} start Start point for the change
+ * @param {boolean} toSubmit If true, mark current selection as sent
+ * @return {ve.dm.Change} The change since start in the client's local history
+ */
+ve.dm.RebaseClient.prototype.getChangeSince = null;
+
+/**
+ * @abstract
+ * @param {number} backtrack Number of rejected changes backtracked 
immediately before this change
+ * @param {ve.dm.Change} change The change to send
+ */
+ve.dm.RebaseClient.prototype.sendChange = null;
+
+/**
+ * Apply a change to the surface, and add it to the history
+ *
+ * @abstract
+ * @param {ve.dm.Change} change The change to apply
+ */
+ve.dm.RebaseClient.prototype.applyChange = null;
+
+/**
+ * Unapply a change from the surface, and remove it from the history
+ *
+ * @abstract
+ * @param {ve.dm.Change} change The change to unapply
+ */
+ve.dm.RebaseClient.prototype.unapplyChange = null;
+
+/**
+ * Add a change to history, without applying it to the surface
+ *
+ * @abstract
+ * @param {ve.dm.Change} change The change to add
+ */
+ve.dm.RebaseClient.prototype.addToHistory = null;
+
+/**
+ * Remove a change from history, without unapplying it to the surface
+ *
+ * @abstract
+ * @param {ve.dm.Change} change The change to remove
+ */
+ve.dm.RebaseClient.prototype.removeFromHistory = null;
+
+/* Methods */
+
+/**
+ * @return {number} Author ID
+ */
+ve.dm.RebaseClient.prototype.getAuthor = function () {
+       return this.author;
+};
+
+/**
+ * @param {number} author Author ID
+ */
+ve.dm.RebaseClient.prototype.setAuthor = function ( author ) {
+       this.author = author;
+};
+
+/**
+ * Submit all outstanding changes
+ *
+ * This will submit all transactions that exist in local history but have not 
been broadcast
+ * by the server.
+ */
+ve.dm.RebaseClient.prototype.submitChange = function () {
+       var change = this.getChangeSince( this.sentLength, true );
+       if ( change.isEmpty() ) {
+               return;
+       }
+       this.sendChange( this.backtrack, change );
+       this.backtrack = 0;
+       this.sentLength += change.getLength();
+};
+
+/**
+ * Accept a committed change from the server
+ *
+ * If the committed change is by the local author, then it is already applied 
to the document
+ * and at the correct point in history: just move the commitLength pointer.
+ *
+ * If the commited change is by a different author, then:
+ * - Rebase local uncommitted changes over the committed change
+ * - If there is a rejected tail, then apply its inverse to the document
+ * - Apply the rebase-transposed committed change to the document
+ * - Rewrite history to have the committed change followed by rebased 
uncommitted changes
+ *
+ * @param {ve.dm.Change} change The committed change from the server
+ */
+ve.dm.RebaseClient.prototype.acceptChange = function ( change ) {
+       var uncommitted, result,
+               author = change.firstAuthor();
+       if ( !author ) {
+               return;
+       }
+
+       if ( author !== this.getAuthor() ) {
+               uncommitted = this.getChangeSince( this.commitLength, false );
+               result = ve.dm.Change.static.rebaseUncommittedChange( change, 
uncommitted );
+               if ( result.rejected ) {
+                       // Undo rejected tail, and mark unsent and backtracked 
if necessary
+                       this.unapplyChange( result.rejected );
+                       uncommitted = uncommitted.truncate( 
result.rejected.start - uncommitted.start );
+                       if ( this.sentLength > result.rejected.start ) {
+                               this.backtrack += this.sentLength - 
result.rejected.start;
+                       }
+                       this.sentLength = result.rejected.start;
+               }
+               // We are already right by definition about our own selection
+               delete result.transposedHistory.selections[ this.getAuthor() ];
+               this.applyChange( result.transposedHistory );
+               // Rewrite history
+               this.removeFromHistory( result.transposedHistory );
+               this.removeFromHistory( uncommitted );
+               this.addToHistory( change );
+               this.addToHistory( result.rebased );
+
+               this.sentLength += change.getLength();
+       }
+       this.commitLength += change.getLength();
+};
diff --git a/src/dm/ve.dm.RebaseServer.js b/src/dm/ve.dm.RebaseServer.js
new file mode 100644
index 0000000..30edf7e
--- /dev/null
+++ b/src/dm/ve.dm.RebaseServer.js
@@ -0,0 +1,86 @@
+/*!
+ * VisualEditor DataModel rebase server class.
+ *
+ * @copyright 2011-2016 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+/* eslint-env node, es6 */
+
+/**
+ * DataModel rebase server
+ *
+ * @class
+ */
+ve.dm.RebaseServer = function VeDmRebaseServer() {
+       this.stateForDoc = new Map();
+};
+
+OO.initClass( ve.dm.RebaseServer );
+
+/* Methods */
+
+/**
+ * Get the state of a document by name.
+ *
+ * @param {string} name Name of a document
+ * @return {Object} Document state (history and selections)
+ * @return {ve.dm.Change} return.history History as one big Change
+ * @return {Map.<number,ve.dm.Change>} return.continueBases Per-author 
transposed history for rebasing
+ * @return {Map.<number,number>} return.rejections Per-author count of 
unacknowledged rejections
+ */
+ve.dm.RebaseServer.prototype.getStateForDoc = function ( name ) {
+       if ( !this.stateForDoc.has( name ) ) {
+               this.stateForDoc.set( name, {
+                       history: new ve.dm.Change( 0, [], [], {} ),
+                       continueBases: new Map(),
+                       rejections: new Map()
+               } );
+       }
+       return this.stateForDoc.get( name );
+};
+
+/**
+ * Attempt to rebase and apply a change to a document.
+ *
+ * The change can be a new change, or a continued change. A continuated change 
means one that
+ * follows on immediately from the author's last submitted change, other than 
possibly being
+ * rebased onto some more recent committed history.
+ *
+ * @param {string} doc Document name
+ * @param {number} author Author ID
+ * @param {number} backtrack How many transactions are backtracked from the 
previous submission
+ * @param {ve.dm.Change} change Change to apply
+ * @return {ve.dm.Change} Accepted change (or initial segment thereof), as 
rebased
+ */
+ve.dm.RebaseServer.prototype.applyChange = function ( doc, author, backtrack, 
change ) {
+       var base, rejections, result,
+               state = this.getStateForDoc( doc );
+
+       base = state.continueBases.get( author ) || change.truncate( 0 );
+       rejections = state.rejections.get( author ) || 0;
+       if ( rejections > backtrack ) {
+               // Follow-on does not fully acknowledge outstanding conflicts: 
reject entirely
+               state.rejections.set( author, rejections - backtrack + 
change.transactions.length );
+               return change.truncate( 0 );
+       }
+       if ( rejections < backtrack ) {
+               throw new Error( 'Backtrack=' + backtrack + ' > ' + rejections 
+ '=rejections' );
+       }
+
+       if ( change.start > base.start ) {
+               // Remote has rebased some committed changes into its history 
since base was built.
+               // They are guaranteed to be equivalent to the start of base. 
See mathematical
+               // docs for proof (Cuius rei demonstrationem mirabilem sane 
deteximus hanc marginis
+               // exiguitas non caperet).
+               base = base.mostRecent( change.start );
+       }
+       base = base.concat( state.history.mostRecent( base.start + 
base.getLength() ) );
+
+       result = ve.dm.Change.static.rebaseUncommittedChange( base, change );
+       state.rejections.set( author, result.rejected ? 
result.rejected.getLength() : 0 );
+       state.continueBases.set( author, result.transposedHistory );
+
+       if ( result.rebased.getLength() ) {
+               state.history.push( result.rebased );
+       }
+       return result.rebased;
+};
diff --git a/tests/dm/ve.dm.RebaseServer.test.js 
b/tests/dm/ve.dm.RebaseServer.test.js
new file mode 100644
index 0000000..8c17932
--- /dev/null
+++ b/tests/dm/ve.dm.RebaseServer.test.js
@@ -0,0 +1,318 @@
+/*!
+ * VisualEditor DataModel Rebase client/server logic tests.
+ *
+ * @copyright 2011-2016 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+QUnit.module( 've.dm.RebaseServer' );
+
+ve.dm.testHistorySummary = function ( change, commitLength, sentLength ) {
+       var committed, sent, unsent,
+               text = [];
+       if ( commitLength === undefined ) {
+               commitLength = change.transactions.length;
+       }
+       if ( sentLength === undefined ) {
+               sentLength = change.transactions.length;
+       }
+       committed = change.transactions.slice( 0, commitLength ),
+       sent = change.transactions.slice( commitLength, sentLength ),
+       unsent = change.transactions.slice( sentLength );
+
+       function joinText( transactions ) {
+               return transactions.map( function ( transaction ) {
+                       return transaction.operations.filter( function ( op ) {
+                               return op.type === 'replace';
+                       } ).map( function ( op ) {
+                               var text = [];
+                               if ( op.remove.length ) {
+                                       text.push( '-(' + op.remove.map( 
function ( item ) {
+                                               return item[ 0 ];
+                                       } ).join( '' ) + ')' );
+                               }
+                               if ( op.insert.length ) {
+                                       text.push( op.insert.map( function ( 
item ) {
+                                               return item[ 0 ];
+                                       } ).join( '' ) );
+                               }
+                               return text.join( '' );
+                       } ).join( '' );
+               } ).join( '' );
+       }
+       if ( committed.length ) {
+               text.push( joinText( committed ) );
+       }
+       if ( sent.length ) {
+               text.push( joinText( sent ) + '?' );
+       }
+       if ( unsent.length ) {
+               text.push( joinText( unsent ) + '!' );
+       }
+       return text.join( '/' );
+};
+
+ve.dm.TestRebaseServer = function VeDmRebaseServer() {
+       ve.dm.RebaseServer.apply( this );
+};
+
+OO.inheritClass( ve.dm.TestRebaseServer, ve.dm.RebaseServer );
+
+ve.dm.TestRebaseServer.prototype.historySummary = function () {
+       return ve.dm.testHistorySummary( this.stateForDoc.get( 'foo' ).history 
);
+};
+
+ve.dm.TestRebaseClient = function VeDmTestRebaseClient( server, sharedIncoming 
) {
+       ve.dm.RebaseClient.apply( this );
+       this.server = server;
+       this.sharedIncoming = sharedIncoming;
+       this.incomingPointer = 0;
+       this.outgoing = [];
+       this.outgoingPointer = 0;
+       this.history = new ve.dm.Change( 0, [], [], {} );
+       this.trueHistory = [];
+};
+
+OO.initClass( ve.dm.TestRebaseClient );
+OO.mixinClass( ve.dm.TestRebaseClient, ve.dm.RebaseClient );
+
+ve.dm.TestRebaseClient.prototype.historySummary = function () {
+       return ve.dm.testHistorySummary( this.history, this.commitLength, 
this.sentLength );
+};
+
+ve.dm.TestRebaseClient.prototype.getChangeSince = function ( start ) {
+       return this.history.mostRecent( start );
+};
+
+ve.dm.TestRebaseClient.prototype.sendChange = function ( backtrack, change ) {
+       this.outgoing.push( { backtrack: backtrack, change: change } );
+};
+
+ve.dm.TestRebaseClient.prototype.applyChange = function ( change ) {
+       var author = this.getAuthor();
+       change.transactions.forEach( function ( transaction ) {
+               if ( transaction.author === null ) {
+                       transaction.author = author;
+               }
+       } );
+       this.history.push( change );
+       this.trueHistory.push( { change: change, reversed: false } );
+};
+
+ve.dm.TestRebaseClient.prototype.unapplyChange = function ( change ) {
+       this.history = this.history.truncate( change.start );
+       this.trueHistory.push( { change: change, reversed: true } );
+};
+
+ve.dm.TestRebaseClient.prototype.addToHistory = function ( change ) {
+       this.history.push( change );
+};
+
+ve.dm.TestRebaseClient.prototype.removeFromHistory = function ( change ) {
+       this.history = this.history.truncate( change.start );
+};
+
+ve.dm.TestRebaseClient.prototype.deliverOne = function () {
+       var item, rebased;
+       item = this.outgoing[ this.outgoingPointer++ ];
+       rebased = this.server.applyChange( 'foo', this.getAuthor(), 
item.backtrack, item.change );
+       if ( !rebased.isEmpty() ) {
+               this.sharedIncoming.push( rebased );
+       }
+};
+
+ve.dm.TestRebaseClient.prototype.receiveOne = function () {
+       this.acceptChange( this.sharedIncoming[ this.incomingPointer++ ] );
+};
+
+QUnit.test( 'Rebase', 43, function ( assert ) {
+       var origData = [ { type: 'paragraph' }, { type: '/paragraph' } ],
+               newSurface = function () {
+                       return new ve.dm.Surface(
+                               ve.dm.example.createExampleDocumentFromData( 
origData )
+                       );
+               },
+               txReplace = function ( before, remove, insert, after ) {
+                       return new ve.dm.Transaction( [
+                               { type: 'retain', length: before },
+                               {
+                                       type: 'replace',
+                                       remove: remove,
+                                       insert: insert,
+                                       insertedDataOffset: 0,
+                                       insertedDataLength: insert.length
+                               },
+                               { type: 'retain', length: after }
+                       ] );
+               },
+               txInsert = function ( before, insert, after ) {
+                       return txReplace( before, [], insert, after );
+               },
+               txRemove = function ( before, remove, after ) {
+                       return txReplace( before, remove, [], after );
+               },
+               noVals = new ve.dm.IndexValueStore(),
+               surface = newSurface(),
+               doc = surface.documentModel,
+               newSel = function ( offset ) {
+                       return new ve.dm.LinearSelection( doc, new ve.Range( 
offset ) );
+               },
+               b = ve.dm.example.bold,
+               i = ve.dm.example.italic,
+               u = ve.dm.example.underline,
+               bIndex = [ ve.dm.example.boldIndex ],
+               iIndex = [ ve.dm.example.italicIndex ],
+               uIndex = [ ve.dm.example.underlineIndex ],
+               bStore = new ve.dm.IndexValueStore( [ b ] ),
+               iStore = new ve.dm.IndexValueStore( [ i ] ),
+               uStore = new ve.dm.IndexValueStore( [ u ] ),
+               server = new ve.dm.TestRebaseServer(),
+               sharedIncoming = [],
+               client1 = new ve.dm.TestRebaseClient( server, sharedIncoming ),
+               client2 = new ve.dm.TestRebaseClient( server, sharedIncoming );
+
+       client1.setAuthor( 1 );
+       client2.setAuthor( 2 );
+
+       // Client historySummary() output looks like: confirmed/sent?/unsent!
+       // Obviously, the server only has confirmed items
+
+       // First, concurrent insertions
+       client1.applyChange( new ve.dm.Change( 0, [
+               txInsert( 1, [ 'a' ], 3 ),
+               txInsert( 2, [ 'b' ], 3 ),
+               txInsert( 3, [ 'c' ], 3 )
+       ], [ noVals, noVals, noVals ], { 1: newSel( 4 ) } ) );
+       assert.equal( client1.historySummary(), 'abc!', '1apply0' );
+       client1.submitChange();
+       assert.equal( client1.historySummary(), 'abc?', '1submit0' );
+       client1.deliverOne();
+       assert.equal( server.historySummary(), 'abc', '1deliver0' );
+
+       client2.applyChange( new ve.dm.Change( 0, [
+               txInsert( 1, [ 'A' ], 3 ),
+               txInsert( 2, [ 'B' ], 3 )
+       ], [ noVals, noVals ], { 2: newSel( 3 ) } ) );
+       assert.equal( client2.historySummary(), 'AB!', '2apply0' );
+       client2.submitChange();
+       client2.deliverOne();
+       assert.equal( server.historySummary(), 'abcAB', '2deliver0' );
+
+       client1.applyChange( new ve.dm.Change( 3, [
+               txInsert( 4, [ [ 'd', bIndex ] ], 3 ),
+               txInsert( 5, [ [ 'e', bIndex ] ], 3 ),
+               txInsert( 6, [ [ 'f', bIndex ] ], 3 )
+       ], [ bStore, noVals, noVals ], { 1: newSel( 7 ) } ) );
+       assert.equal( client1.historySummary(), 'abc?/def!', '1apply1' );
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abc/def!', '1receive0' );
+       client1.submitChange();
+       assert.equal( client1.historySummary(), 'abc/def?', '1receive1' );
+       client1.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdef', '1deliver1' );
+
+       client2.applyChange( new ve.dm.Change( 2, [
+               txInsert( 3, [ [ 'C', uIndex ] ], 3 ),
+               txInsert( 4, [ [ 'D', uIndex ] ], 3 )
+       ], [ uStore, noVals ], { 2: newSel( 5 ) } ) );
+       assert.equal( client2.historySummary(), 'AB?/CD!', '2apply1' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abc/AB?/CD!', '2receive0' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcAB/CD!', '2receive1' );
+
+       client2.submitChange();
+       assert.equal( client2.historySummary(), 'abcAB/CD?', '2submit1' );
+       client2.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCD', '2deliver1' );
+
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abcAB/def?', '1receive1' );
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abcABdef', '1receive2' );
+
+       client1.applyChange( new ve.dm.Change( 8, [
+               txInsert( 9, [ [ 'g', iIndex ] ], 3 ),
+               txInsert( 10, [ [ 'h', iIndex ] ], 3 ),
+               txInsert( 11, [ [ 'i', iIndex ] ], 3 )
+       ], [ iStore, noVals, noVals ], { 1: newSel( 12 ) } ) );
+       assert.equal( client1.historySummary(), 'abcABdef/ghi!', '1apply3' );
+       client1.submitChange();
+       assert.equal( client1.historySummary(), 'abcABdef/ghi?', '1submit3' );
+       client1.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCDghi', '1deliver3' );
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abcABdefCD/ghi?', '1receive3' 
);
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abcABdefCDghi', '1receive4' );
+
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdef/CD?', '2receive2' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCD', '2receive3' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi', '2receive4' );
+
+       // Then, deliver one deletion and leave another in the pipeline
+       client1.applyChange( new ve.dm.Change( 13, [
+               txRemove( 5, [ 'B', 'd' ], 10 )
+       ], [ noVals ], { 1: newSel( 5 ) } ) );
+       assert.equal( client1.historySummary(), 'abcABdefCDghi/-(Bd)!', 
'1apply5' );
+       client1.submitChange();
+       assert.equal( client1.historySummary(), 'abcABdefCDghi/-(Bd)?', 
'1submit5' );
+       client1.applyChange( new ve.dm.Change( 14, [
+               txRemove( 3, [ 'c', 'A' ], 10 )
+       ], [ noVals ], { 1: newSel( 3 ) } ) );
+       assert.equal( client1.historySummary(), 'abcABdefCDghi/-(Bd)?/-(cA)!', 
'1apply6' );
+       client1.submitChange();
+       assert.equal( client1.historySummary(), 'abcABdefCDghi/-(Bd)-(cA)?', 
'1submit6' );
+       client1.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCDghi-(Bd)', 
'1deliver5' );
+       client1.receiveOne();
+       assert.equal( client1.historySummary(), 'abcABdefCDghi-(Bd)/-(cA)?', 
'1receive5' );
+
+       // Apply a partially-conflicting change
+       client2.applyChange( new ve.dm.Change( 13, [
+               txInsert( 1, [ 'W' ], 16 ),
+               // Conflicts with undelivered deletion of 'cA'
+               txInsert( 5, [ 'X' ], 13 ),
+               // Conflicts with delivered deletion of 'Bd'
+               txInsert( 8, [ 'Y' ], 11 ),
+               txInsert( 12, [ 'Z' ], 8 )
+       ], [ noVals, noVals, noVals, noVals ], { 2: newSel( 12 ) } ) );
+       assert.equal( client2.historySummary(), 'abcABdefCDghi/WXYZ!', 
'2apply5' );
+       client2.submitChange();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi/WXYZ?', 
'2submit5' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)/WX?', 
'2receive5' );
+
+       // Apply a "doomed change" built on top of a change that will conflict
+       client2.applyChange( new ve.dm.Change( 16, [
+               txInsert( 1, [ 'V' ], 18 )
+       ], [ noVals ], { 2: newSel( 2 ) } ) );
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)/WX?/V!', 
'2apply7' );
+       client2.submitChange();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)/WXV?', 
'2submit7' );
+
+       client1.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCDghi-(Bd)-(cA)', 
'1deliver6' );
+
+       client2.deliverOne();
+       client2.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCDghi-(Bd)-(cA)W', 
'2deliver5' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)-(cA)/W?', 
'2receive6' );
+
+       client2.applyChange( new ve.dm.Change( 16, [
+               txInsert( 1, [ 'P' ], 16 )
+       ], [ noVals ], { 2: newSel( 2 ) } ) );
+       assert.equal( client2.historySummary(), 
'abcABdefCDghi-(Bd)-(cA)/W?/P!', '2apply8' );
+       client2.submitChange();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)-(cA)/WP?', 
'2submit8' );
+       client2.deliverOne();
+       assert.equal( server.historySummary(), 'abcABdefCDghi-(Bd)-(cA)WP', 
'2deliver8' );
+
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)-(cA)W/P?', 
'2receive7' );
+       client2.receiveOne();
+       assert.equal( client2.historySummary(), 'abcABdefCDghi-(Bd)-(cA)WP', 
'2receive8' );
+} );
diff --git a/tests/index.html b/tests/index.html
index 28a149d..a7cd3b5 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -429,6 +429,8 @@
 
                <!-- visualEditor.rebase -->
                <script src="../src/dm/ve.dm.Change.js"></script>
+               <script src="../src/dm/ve.dm.RebaseServer.js"></script>
+               <script src="../src/dm/ve.dm.RebaseClient.js"></script>
 
                <!-- visualEditor.test -->
                <script src="../tests/ve.qunit.js"></script>
@@ -458,6 +460,7 @@
                <script src="../tests/dm/ve.dm.Transaction.test.js"></script>
                <script 
src="../tests/dm/ve.dm.TransactionBuilder.test.js"></script>
                <script src="../tests/dm/ve.dm.Change.test.js"></script>
+               <script src="../tests/dm/ve.dm.RebaseServer.test.js"></script>
                <script 
src="../tests/dm/ve.dm.TransactionProcessor.test.js"></script>
                <script 
src="../tests/dm/ve.dm.APIResultsQueue.test.js"></script>
                <script src="../tests/dm/ve.dm.Surface.test.js"></script>

-- 
To view, visit https://gerrit.wikimedia.org/r/322714
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I2876abff50eb37a80b8c2407db64a6e39393aed1
Gerrit-PatchSet: 9
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <da...@troi.org>
Gerrit-Reviewer: Catrope <r...@wikimedia.org>
Gerrit-Reviewer: Divec <da...@troi.org>
Gerrit-Reviewer: Esanders <esand...@wikimedia.org>
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to