jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/331423 )

Change subject: Make RebaseServer asynchronous
......................................................................


Make RebaseServer asynchronous

Change-Id: I6f565ab1893a91eb4e4e241fc8ba7f1a27fd96c2
---
M .jsduck/categories.json
M Gruntfile.js
M build/modules.json
M jsduck.json
M rebaser/server.js
M src/dm/ve.dm.RebaseDocState.js
M src/dm/ve.dm.RebaseServer.js
A src/ve.utils-es6.js
M tests/dm/ve.dm.RebaseServer.test.js
M tests/dm/ve.dm.TestRebaseClient.js
M tests/dm/ve.dm.TestRebaseServer.js
M tests/index.html
12 files changed, 357 insertions(+), 145 deletions(-)

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



diff --git a/.jsduck/categories.json b/.jsduck/categories.json
index f5f5ade..44711b7 100644
--- a/.jsduck/categories.json
+++ b/.jsduck/categories.json
@@ -104,7 +104,6 @@
                                "classes": [
                                        "ve.dm.Change",
                                        "ve.dm.RebaseDocState",
-                                       "ve.dm.RebaseServer",
                                        "ve.dm.RebaseClient",
                                        "ve.dm.SurfaceSynchronizer"
                                ]
@@ -246,13 +245,6 @@
                                "classes": [
                                        "ve.ce.TestOffset",
                                        "ve.ce.TestRunner"
-                               ]
-                       },
-                       {
-                               "name": "Rebaser",
-                               "classes": [
-                                       "ve.dm.TestRebaseClient",
-                                       "ve.dm.TestRebaseServer"
                                ]
                        }
                ]
diff --git a/Gruntfile.js b/Gruntfile.js
index d2500f9..7d108e8 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -358,6 +358,7 @@
                                                                
'src/init/**/*.js',
                                                                
'src/ce/**/*.js',
                                                                
'src/ui/**/*.js',
+                                                               
'src/dm/ve.dm.RebaseDocState.js',
                                                                
'src/dm/ve.dm.SurfaceSynchronizer.js',
                                                                
'src/dm/ve.dm.TableSlice.js',
                                                                
'src/dm/annotations/ve.dm.BidiAnnotation.js',
diff --git a/build/modules.json b/build/modules.json
index da2c0de..7f46462 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -638,6 +638,7 @@
        },
        "visualEditor.rebase": {
                "scripts": [
+                       "src/ve.utils-es6.js",
                        "src/dm/ve.dm.Change.js",
                        "src/dm/ve.dm.RebaseDocState.js",
                        "src/dm/ve.dm.RebaseServer.js",
@@ -930,6 +931,7 @@
        },
        "rebaser.build": {
                "scripts": [
+                       "src/ve.utils-es6.js",
                        "src/dm/ve.dm.IndexValueStore.js",
                        "src/dm/ve.dm.Transaction.js",
                        "src/dm/ve.dm.Change.js",
diff --git a/jsduck.json b/jsduck.json
index c7a2067..a74cd5f 100644
--- a/jsduck.json
+++ b/jsduck.json
@@ -9,6 +9,13 @@
        "--processes": "0",
        "--warnings-exit-nonzero": true,
        "--external": 
"HTMLDocument,Window,Node,Text,Set,Range,Selection,ClientRect,File,Blob,DataTransfer,DataTransferItem,KeyboardEvent,MouseEvent",
+       "--exclude": [
+               "src/dm/ve.dm.RebaseServer.js",
+               "src/ve.utils-es6.js",
+               "tests/dm/ve.dm.RebaseServer.test.js",
+               "tests/dm/ve.dm.TestRebaseClient.js",
+               "tests/dm/ve.dm.TestRebaseServer.js"
+       ],
        "--": [
                ".jsduck/external.js",
                "lib/oojs/oojs.jquery.js",
diff --git a/rebaser/server.js b/rebaser/server.js
index c797472..f18a605 100644
--- a/rebaser/server.js
+++ b/rebaser/server.js
@@ -1,6 +1,12 @@
+/*!
+ * VisualEditor rebase server script.
+ *
+ * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
 /* eslint-disable no-console */
 
-var rebaseServer, artificialDelay, logStream,
+var rebaseServer, pendingForDoc, artificialDelay, logStream, handlers,
        port = 8081,
        fs = require( 'fs' ),
        express = require( 'express' ),
@@ -30,86 +36,166 @@
        logEvent( event );
 }
 
+function wait( timeout ) {
+       return new Promise( function ( resolve ) {
+               setTimeout( resolve, timeout );
+       } );
+}
+
+function logError( err ) {
+       console.log( err.stack );
+}
+
 rebaseServer = new ve.dm.RebaseServer( logServerEvent );
+docNamespaces = new Map();
+lastAuthorForDoc = new Map();
+pendingForDoc = new Map();
 artificialDelay = parseInt( process.argv[ 2 ] ) || 0;
+
+function* welcomeNewClient( socket, docName, author ) {
+       var state, authorData;
+       yield rebaseServer.updateDocState( docName, author, null, {
+               displayName: 'User ' + author // TODO: i18n
+       } );
+
+       state = yield rebaseServer.getDocState( docName );
+       authorData = state.authors.get( author );
+
+       socket.emit( 'registered', {
+               authorId: author,
+               authorName: authorData.displayName,
+               token: authorData.token
+       } );
+       docNamespaces.get( docName ).emit( 'nameChange', {
+               authorId: author,
+               authorName: authorData.displayName
+       } );
+       // HACK Catch the client up on the current state by sending it the 
entire history
+       // Ideally we'd be able to initialize the client using HTML, but that's 
hard, see
+       // comments in the /raw handler. Keeping an updated linmod on the 
server could be
+       // feasible if TransactionProcessor was modified to have a "don't sync, 
just apply"
+       // mode and ve.dm.Document was faked with { data: ..., metadata: ..., 
store: ... }
+       socket.emit( 'initDoc', {
+               history: state.history.serialize( true ),
+               names: state.getActiveNames()
+       } );
+}
+
+function* onSubmitChange( context, data ) {
+       var change, applied;
+       yield wait( artificialDelay );
+       change = ve.dm.Change.static.deserialize( data.change, null, true );
+       applied = yield rebaseServer.applyChange( context.docName, 
context.author, data.backtrack, change );
+       if ( !applied.isEmpty() ) {
+               docNamespaces.get( context.docName ).emit( 'newChange', 
applied.serialize( true ) );
+       }
+}
+
+function* onChangeName( context, newName ) {
+       yield rebaseServer.updateDocState( context.docName, context.author, 
null, {
+               displayName: newName
+       } );
+       docNamespaces.get( context.docName ).emit( 'nameChange', {
+               authorId: context.author,
+               authorName: newName
+       } );
+       logServerEvent( {
+               type: 'nameChange',
+               doc: context.docName,
+               author: context.author,
+               newName: newName
+       } );
+}
+
+function* onUsurp( context, data ) {
+       var state = yield rebaseServer.getDocState( context.docName ),
+               newAuthorData = state.authors.get( data.authorId );
+       if ( newAuthorData.token !== data.token ) {
+               context.socket.emit( 'usurpFailed' );
+               return;
+       }
+       yield rebaseServer.updateDocState( context.docName, data.authorId, 
null, {
+               active: true
+       } );
+       // TODO either delete this author, or reimplement usurp in a 
client-initiated way
+       yield rebaseServer.updateDocState( context.docName, context.author, 
null, {
+               active: false
+       } );
+       context.socket.emit( 'registered', {
+               authorId: data.authorId,
+               authorName: newAuthorData.displayName,
+               token: newAuthorData.token
+       } );
+       docNamespaces.get( context.docName ).emit( 'nameChange', {
+               authorId: data.authorId,
+               authorName: newAuthorData.displayName
+       } );
+       docNamespaces.get( context.docName ).emit( 'authorDisconnect', 
context.author );
+
+       context.author = data.authorId;
+}
+
+function* onDisconnect( context ) {
+       yield rebaseServer.updateDocState( context.docName, context.author, 
null, {
+               active: false
+       } );
+       docNamespaces.get( context.docName ).emit( 'authorDisconnect', 
context.author );
+       logServerEvent( {
+               type: 'disconnect',
+               doc: context.docName,
+               author: context.author
+       } );
+}
+
+function addStep( docName, generatorFunc ) {
+       var pending = Promise.resolve( pendingForDoc.get( docName ) );
+       pending = pending
+               .then( function () {
+                       return ve.spawn( generatorFunc );
+               } )
+               .catch( logError );
+       pendingForDoc.set( pending );
+}
+
+handlers = {
+       submitChange: onSubmitChange,
+       changeName: onChangeName,
+       usurp: onUsurp,
+       disconnect: onDisconnect
+};
+
+function handleEvent( context, eventName, data ) {
+       addStep( context.docName, handlers[ eventName ]( context, data ) );
+}
 
 function makeConnectionHandler( docName ) {
        return function handleConnection( socket ) {
-               var history = rebaseServer.getDocState( docName ).history,
-                       author = 1 + ( lastAuthorForDoc.get( docName ) || 0 ),
-                       authorData = rebaseServer.getAuthorData( docName, 
author );
-               lastAuthorForDoc.set( docName, author );
-               rebaseServer.setAuthorName( docName, author, 'User ' + author 
); // TODO: i18n
+               // Allocate new author ID
+               var context = {
+                               socket: socket,
+                               docName: docName,
+                               author: 1 + ( lastAuthorForDoc.get( docName ) 
|| 0 )
+                       },
+                       eventName;
+               lastAuthorForDoc.set( docName, context.author );
                logServerEvent( {
                        type: 'newClient',
                        doc: docName,
-                       author: author
+                       author: context.author
                } );
-               socket.emit( 'registered', {
-                       authorId: author,
-                       authorName: authorData.displayName,
-                       token: authorData.token
-               } );
-               docNamespaces.get( docName ).emit( 'nameChange', { authorId: 
author, authorName: authorData.displayName } );
-               socket.on( 'usurp', function ( data ) {
-                       var newAuthorData = rebaseServer.getAuthorData( 
docName, data.authorId );
-                       if ( newAuthorData.token !== data.token ) {
-                               socket.emit( 'usurpFailed' );
-                               return;
-                       }
-                       newAuthorData.active = true;
-                       socket.emit( 'registered', {
-                               authorId: data.authorId,
-                               authorName: newAuthorData.displayName,
-                               token: newAuthorData.token
-                       } );
-                       docNamespaces.get( docName ).emit( 'nameChange', { 
authorId: data.authorId, authorName: newAuthorData.displayName } );
-                       docNamespaces.get( docName ).emit( 'authorDisconnect', 
author );
-                       rebaseServer.removeAuthor( docName, author );
-                       author = data.authorId;
-               } );
-               // HACK Catch the client up on the current state by sending it 
the entire history
-               // Ideally we'd be able to initialize the client using HTML, 
but that's hard, see
-               // comments in the /raw handler. Keeping an updated linmod on 
the server could be
-               // feasible if TransactionProcessor was modified to have a 
"don't sync, just apply"
-               // mode and ve.dm.Document was faked with { data: ..., 
metadata: ..., store: ... }
-               socket.emit( 'initDoc', { history: history.serialize( true ), 
names: rebaseServer.getAllNames( docName ) } );
-               socket.on( 'changeName', function ( newName ) {
-                       logServerEvent( {
-                               type: 'nameChange',
-                               doc: docName,
-                               author: author,
-                               newName: newName
-                       } );
-                       rebaseServer.setAuthorName( docName, author, newName );
-                       docNamespaces.get( docName ).emit( 'nameChange', { 
authorId: author, authorName: newName } );
-               } );
-               socket.on( 'submitChange', setTimeout.bind( null, function ( 
data ) {
-                       var change, applied;
-                       try {
-                               change = ve.dm.Change.static.deserialize( 
data.change, null, true );
-                               applied = rebaseServer.applyChange( docName, 
author, data.backtrack, change );
-                               if ( !applied.isEmpty() ) {
-                                       docNamespaces.get( docName ).emit( 
'newChange', applied.serialize( true ) );
-                               }
-                       } catch ( error ) {
-                               console.error( error.stack );
-                       }
-               }, artificialDelay ) );
+
+               // Kick off welcome process
+               addStep( docName, welcomeNewClient( socket, docName, 
context.author ) );
+
+               // Attach event handlers
+               for ( eventName in handlers ) {
+                       // eslint-disable-next-line no-loop-func
+                       socket.on( eventName, handleEvent.bind( null, context, 
eventName ) );
+               }
                socket.on( 'logEvent', function ( event ) {
-                       event.clientId = author;
+                       event.clientId = context.author;
                        event.doc = docName;
                        logEvent( event );
-               } );
-               socket.on( 'disconnect', function () {
-                       var authorData = rebaseServer.getAuthorData( docName, 
author );
-                       authorData.active = false;
-                       docNamespaces.get( docName ).emit( 'authorDisconnect', 
author );
-                       logServerEvent( {
-                               type: 'disconnect',
-                               doc: docName,
-                               author: author
-                       } );
                } );
        };
 }
diff --git a/src/dm/ve.dm.RebaseDocState.js b/src/dm/ve.dm.RebaseDocState.js
index 23d8e5f..4160853 100644
--- a/src/dm/ve.dm.RebaseDocState.js
+++ b/src/dm/ve.dm.RebaseDocState.js
@@ -28,3 +28,38 @@
 /* Inheritance */
 
 OO.initClass( ve.dm.RebaseDocState );
+
+/* Static Methods */
+
+/**
+ * Get new empty author data object
+ *
+ * @return {Object} New empty author data object
+ * @return {string} return.displayName Display name
+ * @return {number} return.rejections Number of unacknowledged rejections
+ * @return {ve.dm.Change|null} return.continueBase Continue base
+ * @return {string} return.token Secret token for usurping sessions
+ * @return {boolean} return.active Whether the author is active
+ */
+ve.dm.RebaseDocState.static.newAuthorData = function () {
+       return {
+               displayName: '',
+               rejections: 0,
+               continueBase: null,
+               // TODO use cryptographic randomness here and convert to hex
+               token: Math.random().toString(),
+               active: true
+       };
+};
+
+/* Methods */
+
+ve.dm.RebaseDocState.prototype.getActiveNames = function () {
+       var result = {};
+       this.authors.forEach( function ( authorData, authorId ) {
+               if ( authorData.active ) {
+                       result[ authorId ] = authorData.displayName;
+               }
+       } );
+       return result;
+};
diff --git a/src/dm/ve.dm.RebaseServer.js b/src/dm/ve.dm.RebaseServer.js
index a0ad936..366ff45 100644
--- a/src/dm/ve.dm.RebaseServer.js
+++ b/src/dm/ve.dm.RebaseServer.js
@@ -3,7 +3,8 @@
  *
  * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
  */
-/* eslint-env node, es6 */
+
+/* eslint-env es6 */
 
 /**
  * DataModel rebase server
@@ -26,28 +27,13 @@
  * Get the state of a document by name.
  *
  * @param {string} doc Name of a document
- * @return {ve.dm.RebaseDocState} Document state
+ * @return {Promise<ve.dm.RebaseDocState>} Document state
  */
 ve.dm.RebaseServer.prototype.getDocState = function ( doc ) {
        if ( !this.stateForDoc.has( doc ) ) {
                this.stateForDoc.set( doc, new ve.dm.RebaseDocState() );
        }
-       return this.stateForDoc.get( doc );
-};
-
-ve.dm.RebaseServer.prototype.getAuthorData = function ( doc, author ) {
-       var state = this.getDocState( doc );
-       if ( !state.authors.has( author ) ) {
-               state.authors.set( author, {
-                       displayName: '',
-                       rejections: 0,
-                       continueBase: null,
-                       // TODO use cryptographic randomness here and convert 
to hex
-                       token: Math.random(),
-                       active: true
-               } );
-       }
-       return state.authors.get( author );
+       return Promise.resolve( this.stateForDoc.get( doc ) );
 };
 
 /**
@@ -56,38 +42,29 @@
  * @param {string} doc Name of a document
  * @param {number} author Author ID
  * @param {ve.dm.Change} [newHistory] New history to append
- * @param {number} [rejections] Unacknowledged rejections for author
- * @param {ve.dm.Change} [continueBase] Continue base for author
+ * @param {Object} [authorDataChanges] New values for author data (modified 
keys only)
+ * @return {Promise<undefined>}
  */
-ve.dm.RebaseServer.prototype.updateDocState = function ( doc, author, 
newHistory, rejections, continueBase ) {
-       var state = this.getDocState( doc ),
-               authorData = state.authors.get( author );
+ve.dm.RebaseServer.prototype.updateDocState = ve.async( function* 
updateDocState( doc, author, newHistory, authorDataChanges ) {
+       var key, authorData,
+               state = yield this.getDocState( doc );
        if ( newHistory ) {
                state.history.push( newHistory );
        }
-       if ( rejections !== undefined ) {
-               authorData.rejections = rejections;
-       }
-       if ( continueBase ) {
-               authorData.continueBase = continueBase;
-       }
-};
 
-ve.dm.RebaseServer.prototype.setAuthorName = function ( doc, authorId, 
authorName ) {
-       var authorData = this.getAuthorData( doc, authorId );
-       authorData.displayName = authorName;
-};
-
-ve.dm.RebaseServer.prototype.getAllNames = function ( doc ) {
-       var result = {},
-               state = this.getDocState( doc );
-       state.authors.forEach( function ( authorData, authorId ) {
-               if ( authorData.active ) {
-                       result[ authorId ] = authorData.displayName;
+       authorData = state.authors.get( author );
+       if ( !authorData ) {
+               authorData = state.constructor.static.newAuthorData();
+               state.authors.set( author, authorData );
+       }
+       if ( authorDataChanges ) {
+               for ( key in authorData ) {
+                       if ( authorDataChanges[ key ] !== undefined ) {
+                               authorData[ key ] = authorDataChanges[ key ];
+                       }
                }
-       } );
-       return result;
-};
+       }
+} );
 
 /**
  * Attempt to rebase and apply a change to a document.
@@ -100,19 +77,19 @@
  * @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
+ * @return {Promise<ve.dm.Change>} Accepted change (or initial segment 
thereof), as rebased
  */
-ve.dm.RebaseServer.prototype.applyChange = function applyChange( doc, author, 
backtrack, change ) {
+ve.dm.RebaseServer.prototype.applyChange = ve.async( function* applyChange( 
doc, author, backtrack, change ) {
        var base, rejections, result, appliedChange,
-               state = this.getDocState( doc ),
-               authorData = this.getAuthorData( doc, author );
+               state = yield this.getDocState( doc ),
+               authorData = state.authors.get( author );
 
        base = authorData.continueBase || change.truncate( 0 );
        rejections = authorData.rejections || 0;
        if ( rejections > backtrack ) {
                // Follow-on does not fully acknowledge outstanding conflicts: 
reject entirely
                rejections = rejections - backtrack + 
change.transactions.length;
-               this.updateDocState( doc, author, null, rejections, null );
+               yield this.updateDocState( doc, author, null, { rejections: 
rejections } );
                // FIXME argh this publishes an empty change, which is not what 
we want
                appliedChange = state.history.truncate( 0 );
        } else if ( rejections < backtrack ) {
@@ -129,7 +106,10 @@
 
                result = ve.dm.Change.static.rebaseUncommittedChange( base, 
change );
                rejections = result.rejected ? result.rejected.getLength() : 0;
-               this.updateDocState( doc, author, result.rebased, rejections, 
result.transposedHistory );
+               yield this.updateDocState( doc, author, result.rebased, {
+                       rejections: rejections,
+                       continueBase: result.transposedHistory
+               } );
                appliedChange = result.rebased;
        }
        this.logEvent( {
@@ -142,9 +122,4 @@
                rejections: rejections
        } );
        return appliedChange;
-};
-
-ve.dm.RebaseServer.prototype.removeAuthor = function ( doc, author ) {
-       var state = this.getDocState( doc );
-       state.authors.delete( author );
-};
+} );
diff --git a/src/ve.utils-es6.js b/src/ve.utils-es6.js
new file mode 100644
index 0000000..fc33db5
--- /dev/null
+++ b/src/ve.utils-es6.js
@@ -0,0 +1,84 @@
+/*!
+ * VisualEditor ES6 utilities.
+ *
+ * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/* eslint-env es6 */
+
+/**
+ * Run to completion a thenable-yielding iterator
+ *
+ * Each value yielded by the iterator is wrapped in a promise, the result of 
which is fed into
+ * iterator.next/iterator.throw . For thenable values, this has the effect of 
pausing execution
+ * until the thenable resolves.
+ *
+ * Both ve.spawn and ve.async bridge between async functions using yield and 
normal functions
+ * using explicit promises. Use ve.spawn( iterator ).then( ... ) to wrap the 
iterator of an
+ * async function that is already running, and funcName = ve.async( function* 
(...) {...} ) to
+ * get a promise-returning function from an async function.
+ *
+ *     @example
+ *     ve.spawn( function* ( url, filename ) {
+ *             var data = yield get( url );
+ *             yield save( filename, data );
+ *             return data.length;
+ *     }() ).then( function ( data ) {
+ *             console.log( data );
+ *     } ).catch( function ( err ) {
+ *             console.error( err );
+ *     } );
+ *
+ * @param {Object} iterator An iterator that may yield promises
+ * @return {Promise} Promise resolving on the iterator's return/throw value
+ */
+ve.spawn = function ( iterator ) {
+       return new Promise( function ( resolve, reject ) {
+               var resumeNext, resumeThrow;
+               function resume( method, value ) {
+                       var result;
+                       try {
+                               result = method.call( iterator, value );
+                               if ( result.done ) {
+                                       resolve( result.value );
+                               } else {
+                                       Promise.resolve( result.value ).then( 
resumeNext, resumeThrow );
+                               }
+                       } catch ( err ) {
+                               reject( err );
+                       }
+               }
+               resumeNext = result => resume( iterator.next, result );
+               resumeThrow = err => resume( iterator.throw, err );
+               resumeNext();
+       } );
+};
+
+/**
+ * Wrap a thenable-yielding generator function to make an async function
+ *
+ * Both ve.spawn and ve.async bridge between async functions using yield and 
normal functions
+ * using explicit promises. Use ve.spawn( iterator ).then( ... ) to wrap the 
iterator of an
+ * async function that is already running, and funcName = ve.async( function* 
(...) {...} ) to
+ * get a promise-returning function from an async function.
+ *
+ *     @example
+ *     f = ve.async( function* ( url, filename ) {
+ *             var data = yield get( url );
+ *             yield save( filename, data );
+ *             return data.length;
+ *     };
+ *     f().then( function ( data ) {
+ *             console.log( data );
+ *     } ).catch( function ( err ) {
+ *             console.error( err );
+ *     } );
+ *
+ * @param {Function} generator A generator function
+ * @return {Function} Function returning a promise resolving on the 
generator's return/throw value
+ */
+ve.async = function ( generator ) {
+       return function () {
+               return ve.spawn( generator.apply( this, arguments ) );
+       };
+};
diff --git a/tests/dm/ve.dm.RebaseServer.test.js 
b/tests/dm/ve.dm.RebaseServer.test.js
index 4b8c554..242503d 100644
--- a/tests/dm/ve.dm.RebaseServer.test.js
+++ b/tests/dm/ve.dm.RebaseServer.test.js
@@ -4,10 +4,12 @@
  * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
  */
 
+/* eslint-env es6 */
+
 QUnit.module( 've.dm.RebaseServer' );
 
-QUnit.test( 'Rebase', function ( assert ) {
-       var i, j, op, server, client, clients, action, txs,
+QUnit.test( 'Rebase', assert => ve.spawn( function* () {
+       var i, j, op, server, client, clients, action, txs, summary,
                cases = [
                        {
                                name: 'Concurrent insertions',
@@ -299,13 +301,18 @@
                return builder.getTransaction();
        }
 
+       // HACK: A version of spawn that supports this would be better
+       ve.dm.RebaseServer.qunitAssertAsync = assert.async();
+
        for ( i = 0; i < cases.length; i++ ) {
                server = new ve.dm.TestRebaseServer();
-               clients = { server: server };
+               clients = {};
                for ( j = 0; j < cases[ i ].clients.length; j++ ) {
                        client = new ve.dm.TestRebaseClient( server, cases[ i 
].initialData );
                        client.setAuthor( cases[ i ].clients[ j ] );
                        clients[ cases[ i ].clients[ j ] ] = client;
+                       // Initialize
+                       server.updateDocState( 
ve.dm.TestRebaseServer.static.fakeDocName, cases[ i ].clients[ j ] );
                }
 
                for ( j = 0; j < cases[ i ].ops.length; j++ ) {
@@ -326,11 +333,16 @@
                                        client.applyChange( 
ve.dm.Change.static.deserialize( op[ 2 ] ) );
                                }
                        } else if ( action === 'assertHist' ) {
-                               assert.equal( client.getHistorySummary(), op[ 2 
], cases[ i ].name + ': ' + ( op[ 3 ] || j ) );
+                               if ( op[ 0 ] === 'server' ) {
+                                       summary = yield 
server.getHistorySummary();
+                               } else {
+                                       summary = client.getHistorySummary();
+                               }
+                               assert.equal( summary, op[ 2 ], cases[ i ].name 
+ ': ' + ( op[ 3 ] || j ) );
                        } else if ( action === 'submit' ) {
                                client.submitChange();
                        } else if ( action === 'deliver' ) {
-                               client.deliverOne();
+                               yield client.deliverOne();
                        } else if ( action === 'receive' ) {
                                client.receiveOne();
                        } else if ( action === 'assert' ) {
@@ -338,4 +350,8 @@
                        }
                }
        }
-} );
+}() ).catch( function ( err ) {
+       assert.ok( false, err.stack );
+} ).then( function () {
+       ve.dm.RebaseServer.qunitAssertAsync();
+} ) );
diff --git a/tests/dm/ve.dm.TestRebaseClient.js 
b/tests/dm/ve.dm.TestRebaseClient.js
index b0af2bb..1868d6e 100644
--- a/tests/dm/ve.dm.TestRebaseClient.js
+++ b/tests/dm/ve.dm.TestRebaseClient.js
@@ -4,6 +4,8 @@
  * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
  */
 
+/* eslint-env es6 */
+
 /**
  * Rebase client used for testing
  *
@@ -134,14 +136,19 @@
        change.removeFromHistory( this.doc );
 };
 
-ve.dm.TestRebaseClient.prototype.deliverOne = function () {
+ve.dm.TestRebaseClient.prototype.deliverOne = ve.async( function* () {
        var item, rebased;
        item = this.outgoing[ this.outgoingPointer++ ];
-       rebased = this.server.applyChange( 'foo', this.getAuthor(), 
item.backtrack, item.change );
+       rebased = yield this.server.applyChange(
+               ve.dm.TestRebaseServer.static.fakeDocName,
+               this.getAuthor(),
+               item.backtrack,
+               item.change
+       );
        if ( !rebased.isEmpty() ) {
                this.server.incoming.push( rebased );
        }
-};
+} );
 
 ve.dm.TestRebaseClient.prototype.receiveOne = function () {
        this.acceptChange( this.server.incoming[ this.incomingPointer++ ] );
diff --git a/tests/dm/ve.dm.TestRebaseServer.js 
b/tests/dm/ve.dm.TestRebaseServer.js
index 7f893a7..86508ca 100644
--- a/tests/dm/ve.dm.TestRebaseServer.js
+++ b/tests/dm/ve.dm.TestRebaseServer.js
@@ -4,6 +4,8 @@
  * @copyright 2011-2017 VisualEditor Team and others; see 
http://ve.mit-license.org
  */
 
+/* eslint-env es6 */
+
 /**
  * Rebase client used for testing
  *
@@ -20,6 +22,10 @@
 
 OO.inheritClass( ve.dm.TestRebaseServer, ve.dm.RebaseServer );
 
-ve.dm.TestRebaseServer.prototype.getHistorySummary = function historySummary() 
{
-       return ve.dm.TestRebaseClient.static.historySummary( this.getDocState( 
'foo' ).history );
-};
+ve.dm.TestRebaseServer.static.fakeDocName = 'foo';
+
+ve.dm.TestRebaseServer.prototype.getHistorySummary = ve.async( function* 
historySummary() {
+       return ve.dm.TestRebaseClient.static.historySummary(
+               ( yield this.getDocState( this.constructor.static.fakeDocName ) 
).history
+       );
+} );
diff --git a/tests/index.html b/tests/index.html
index f54e1d6..dac6e9f 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -437,6 +437,7 @@
                <script src="../lib/socket.io-client/socket.io.min.js"></script>
 
                <!-- visualEditor.rebase -->
+               <script src="../src/ve.utils-es6.js"></script>
                <script src="../src/dm/ve.dm.Change.js"></script>
                <script src="../src/dm/ve.dm.RebaseDocState.js"></script>
                <script src="../src/dm/ve.dm.RebaseServer.js"></script>

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I6f565ab1893a91eb4e4e241fc8ba7f1a27fd96c2
Gerrit-PatchSet: 19
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: Tchanders <thalia.e.c...@googlemail.com>
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