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