mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-12 09:09:25 +00:00
Use serialization cache in MW integration
Add prepareCacheKey() which submits HTML for serialization and saves the resulting cache key, and tryWithPreparedCacheKey() which uses that cache key (if available; if pending, it waits for it) for API requests. Implemented save(), serialize() and showChanges() in terms of tryWithPreparedCacheKey(). When opening the save dialog, run the conversion, cache it, and fire off a prepareCacheKey(). Then use the cached conversion for save/diff/ serialize. This means we don't convert multiple times, and it causes the prepared wikitext to be used. Bug: 55979 Bug: 56011 Change-Id: I1d56fe88d312e9810a57d56a285ccdf4f1facf42
This commit is contained in:
parent
10702d9fa0
commit
1919fffc74
|
@ -755,7 +755,6 @@ ve.init.mw.ViewPageTarget.prototype.updateToolbarSaveButtonState = function () {
|
|||
* @method
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
|
||||
var doc = this.surface.getModel().getDocument();
|
||||
this.sanityCheckVerified = true;
|
||||
this.saveDialog.setSanityCheck( this.sanityCheckVerified );
|
||||
|
||||
|
@ -769,14 +768,9 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
|
|||
this.saveDialog.$loadingIcon.show();
|
||||
if ( this.pageExists ) {
|
||||
// Has no callback, handled via target.onShowChanges
|
||||
this.showChanges(
|
||||
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() )
|
||||
);
|
||||
this.showChanges( this.docToSave );
|
||||
} else {
|
||||
this.serialize(
|
||||
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
|
||||
ve.bind( this.onSerialize, this )
|
||||
);
|
||||
this.serialize( this.docToSave, ve.bind( this.onSerialize, this ) );
|
||||
}
|
||||
} else {
|
||||
this.saveDialog.swapPanel( 'review' );
|
||||
|
@ -801,8 +795,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogSave = function () {
|
|||
* Try to save the current document.
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
|
||||
var doc = this.surface.getModel().getDocument(),
|
||||
saveOptions = this.getSaveOptions();
|
||||
var saveOptions = this.getSaveOptions();
|
||||
|
||||
// Reset any old captcha data
|
||||
if ( this.captcha ) {
|
||||
|
@ -824,10 +817,7 @@ ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
|
|||
} else {
|
||||
this.saveDialog.saveButton.setDisabled( true );
|
||||
this.saveDialog.$loadingIcon.show();
|
||||
this.save(
|
||||
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
|
||||
saveOptions
|
||||
);
|
||||
this.save( this.docToSave, saveOptions );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -847,7 +837,7 @@ ve.init.mw.ViewPageTarget.prototype.editSource = function () {
|
|||
}
|
||||
// Get Wikitext from the DOM
|
||||
this.serialize(
|
||||
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
|
||||
this.docToSave || ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
|
||||
ve.bind( function ( wikitext ) {
|
||||
var $form,
|
||||
options = this.getSaveOptions(),
|
||||
|
@ -880,11 +870,10 @@ ve.init.mw.ViewPageTarget.prototype.editSource = function () {
|
|||
*
|
||||
* @method
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict= function () {
|
||||
var doc = this.surface.getModel().getDocument();
|
||||
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict = function () {
|
||||
// Get Wikitext from the DOM, and set up a submit call when it's done
|
||||
this.serialize(
|
||||
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
|
||||
this.docToSave,
|
||||
ve.bind( function ( wikitext ) {
|
||||
this.submit( wikitext, this.getSaveOptions() );
|
||||
}, this )
|
||||
|
@ -1247,6 +1236,15 @@ ve.init.mw.ViewPageTarget.prototype.setupSaveDialog = function () {
|
|||
* @method
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
|
||||
// Preload the serialization
|
||||
var doc = this.surface.getModel().getDocument();
|
||||
if ( !this.docToSave ) {
|
||||
this.docToSave = ve.dm.converter.getDomFromData(
|
||||
doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace()
|
||||
);
|
||||
}
|
||||
this.prepareCacheKey( this.docToSave );
|
||||
|
||||
this.saveDialog.setSanityCheck( this.sanityCheckVerified );
|
||||
this.surface.getDialogs().getWindow( 'mwSave' ).open();
|
||||
this.timings.saveDialogOpen = ve.now();
|
||||
|
@ -1259,6 +1257,17 @@ ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
|
|||
* Respond to the save dialog being closed.
|
||||
*/
|
||||
ve.init.mw.ViewPageTarget.prototype.onSaveDialogClose = function () {
|
||||
// Clear the cached HTML and cache key once the document changes
|
||||
var clear = ve.bind( function () {
|
||||
this.docToSave = null;
|
||||
this.clearPreparedCacheKey();
|
||||
}, this );
|
||||
if ( this.surface ) {
|
||||
this.surface.getModel().getDocument().once( 'transact', clear );
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
|
||||
ve.track( 'behavior.saveDialogClose', { 'duration': ve.now() - this.timings.saveDialogOpen } );
|
||||
this.timings.saveDialogOpen = null;
|
||||
};
|
||||
|
|
|
@ -58,8 +58,10 @@ ve.init.mw.Target = function VeInitMwTarget( $container, pageName, revisionId )
|
|||
|
||||
this.pluginCallbacks = [];
|
||||
this.modulesReady = $.Deferred();
|
||||
this.preparedCacheKeyPromise = null;
|
||||
this.loading = false;
|
||||
this.saving = false;
|
||||
this.diffing = false;
|
||||
this.serializing = false;
|
||||
this.submitting = false;
|
||||
this.baseTimeStamp = null;
|
||||
|
@ -346,6 +348,7 @@ ve.init.mw.Target.onSaveError = function ( jqXHR, status, data ) {
|
|||
*/
|
||||
ve.init.mw.Target.onShowChanges = function ( response ) {
|
||||
var data = response.visualeditor;
|
||||
this.diffing = false;
|
||||
if ( !data && !response.error ) {
|
||||
ve.init.mw.Target.onShowChangesError.call( this, null, 'Invalid response from server', null );
|
||||
} else if ( response.error ) {
|
||||
|
@ -366,7 +369,7 @@ ve.init.mw.Target.onShowChanges = function ( response ) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Handle errors during saveChanges action.
|
||||
* Handle errors during showChanges action.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
|
@ -377,7 +380,7 @@ ve.init.mw.Target.onShowChanges = function ( response ) {
|
|||
* @fires showChangesError
|
||||
*/
|
||||
ve.init.mw.Target.onShowChangesError = function ( jqXHR, status, error ) {
|
||||
this.saving = false;
|
||||
this.diffing = false;
|
||||
this.emit( 'showChangesError', jqXHR, status, error );
|
||||
};
|
||||
|
||||
|
@ -530,14 +533,14 @@ ve.init.mw.Target.prototype.load = function () {
|
|||
start = ve.now();
|
||||
|
||||
this.loading = $.ajax( {
|
||||
'url': this.apiUrl,
|
||||
'data': data,
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000,
|
||||
'cache': 'false'
|
||||
} )
|
||||
'url': this.apiUrl,
|
||||
'data': data,
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000,
|
||||
'cache': 'false'
|
||||
} )
|
||||
.then( function ( data, status, jqxhr ) {
|
||||
ve.track( 'performance.system.domLoad', {
|
||||
'bytes': $.byteLength( jqxhr.responseText ),
|
||||
|
@ -553,6 +556,162 @@ ve.init.mw.Target.prototype.load = function () {
|
|||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serialize the current document and store the result in the serialization cache on the server.
|
||||
*
|
||||
* This function returns a promise that is resolved once serialization is complete, with the
|
||||
* cache key passed as the first parameter.
|
||||
*
|
||||
* If there's already a request pending for the same (reference-identical) HTMLDocument, this
|
||||
* function will not initiate a new request but will return the promise for the pending request.
|
||||
* If a request for the same document has already been completed, this function will keep returning
|
||||
* the same promise (which will already have been resolved) until clearPreparedCacheKey() is called.
|
||||
*
|
||||
* @param {HTMLDocument} doc Document to serialize
|
||||
* @returns {jQuery.Promise} Abortable promise, resolved with the cache key.
|
||||
*/
|
||||
ve.init.mw.Target.prototype.prepareCacheKey = function ( doc ) {
|
||||
var xhr, html, start = ve.now(), deferred = $.Deferred();
|
||||
|
||||
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
|
||||
return this.preparedCacheKeyPromise;
|
||||
}
|
||||
this.clearPreparedCacheKey();
|
||||
|
||||
html = this.getHtml( doc );
|
||||
xhr = $.ajax( {
|
||||
'url': this.apiUrl,
|
||||
'data': {
|
||||
'action': 'visualeditor',
|
||||
'paction': 'serializeforcache',
|
||||
'html': html,
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid,
|
||||
'format': 'json'
|
||||
},
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000,
|
||||
'cache': 'false'
|
||||
} )
|
||||
.done( function ( response ) {
|
||||
var trackData = { 'duration': ve.now() - start };
|
||||
if ( response.visualeditor && typeof response.visualeditor.cachekey === 'string' ) {
|
||||
ve.track( 'performance.system.serializeforcache', trackData );
|
||||
deferred.resolve( response.visualeditor.cachekey );
|
||||
} else {
|
||||
ve.track( 'performance.system.serializeforcache.nocachekey', trackData );
|
||||
deferred.reject();
|
||||
}
|
||||
} )
|
||||
.fail( function () {
|
||||
ve.track( 'performance.system.serializeforcache.fail', { 'duration': ve.now() - start } );
|
||||
deferred.reject();
|
||||
} );
|
||||
|
||||
this.preparedCacheKeyPromise = deferred.promise( {
|
||||
'abort': xhr.abort,
|
||||
'html': html,
|
||||
'doc': doc
|
||||
} );
|
||||
return this.preparedCacheKeyPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the prepared wikitext, if any. Same as prepareWikitext() but does not initiate a request
|
||||
* if one isn't already pending or finished. Instead, it returns a rejected promise in that case.
|
||||
*
|
||||
* @param {HTMLDocument} doc Document to serialize
|
||||
* @returns {jQuery.Promise} Abortable promise, resolved with the cache key.
|
||||
*/
|
||||
ve.init.mw.Target.prototype.getPreparedCacheKey = function ( doc ) {
|
||||
var deferred;
|
||||
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
|
||||
return this.preparedCacheKeyPromise;
|
||||
}
|
||||
deferred = $.Deferred();
|
||||
deferred.reject();
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the promise for the prepared wikitext cache key, and abort it if it's still in progress.
|
||||
*/
|
||||
ve.init.mw.Target.prototype.clearPreparedCacheKey = function () {
|
||||
if ( this.preparedCacheKeyPromise ) {
|
||||
this.preparedCacheKeyPromise.abort();
|
||||
this.preparedCacheKeyPromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Try submitting an API request with a cache key for prepared wikitext, falling back to submitting
|
||||
* HTML directly if there is no cache key present or pending, or if the request for the cache key
|
||||
* fails, or if using the cache key fails with a badcachekey error.
|
||||
*
|
||||
* @param {HTMLDocument} doc Document to submit
|
||||
* @param {Object} options POST parameters to send. Do not include 'html', 'cachekey' or 'format'.
|
||||
* @param {string} [eventName] If set, log an event when the request completes successfully. The
|
||||
* full event name used will be 'performance.system.{eventName}.withCacheKey' or .withoutCacheKey
|
||||
* depending on whether or not a cache key was used.
|
||||
* @returns {jQuery.Promise}
|
||||
*/
|
||||
ve.init.mw.Target.prototype.tryWithPreparedCacheKey = function ( doc, options, eventName ) {
|
||||
var data, preparedCacheKey = this.getPreparedCacheKey( doc ), target = this;
|
||||
data = $.extend( {}, options, { 'format': 'json' } );
|
||||
|
||||
function ajaxRequest( cachekey ) {
|
||||
var start = ve.now();
|
||||
if ( typeof cachekey === 'string' ) {
|
||||
data.cachekey = cachekey;
|
||||
} else {
|
||||
// Getting a cache key failed, fall back to sending the HTML
|
||||
data.html = preparedCacheKey && preparedCacheKey.html || target.getHtml( doc );
|
||||
// If using the cache key fails, we'll come back here with cachekey still set
|
||||
delete data.cachekey;
|
||||
}
|
||||
return $.ajax( {
|
||||
'url': target.apiUrl,
|
||||
'data': data,
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000
|
||||
} )
|
||||
.then( function ( response, status, jqxhr ) {
|
||||
var fullEventName, eventData = {
|
||||
'bytes': $.byteLength( jqxhr.responseText ),
|
||||
'duration': ve.now() - start,
|
||||
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
|
||||
};
|
||||
if ( response.error && response.error.code === 'badcachekey' ) {
|
||||
// Log the failure if eventName was set
|
||||
if ( eventName ) {
|
||||
fullEventName = 'performance.system.' + eventName + '.badCacheKey';
|
||||
ve.track( fullEventName, eventData );
|
||||
}
|
||||
// This cache key is evidently bad, clear it
|
||||
target.clearPreparedCacheKey();
|
||||
// Try again without a cache key
|
||||
return ajaxRequest( null );
|
||||
}
|
||||
|
||||
// Log data about the request if eventName was set
|
||||
if ( eventName ) {
|
||||
fullEventName = 'performance.system.' + eventName +
|
||||
( typeof cachekey === 'string' ? '.withCacheKey' : '.withoutCacheKey' );
|
||||
ve.track( fullEventName, eventData );
|
||||
}
|
||||
return jqxhr;
|
||||
} );
|
||||
}
|
||||
|
||||
// If we successfully get prepared wikitext, then invoke ajaxRequest() with the cache key,
|
||||
// otherwise invoke it without.
|
||||
return preparedCacheKey.then( ajaxRequest, ajaxRequest );
|
||||
};
|
||||
|
||||
/**
|
||||
* Post DOM data to the Parsoid API.
|
||||
*
|
||||
|
@ -569,42 +728,22 @@ ve.init.mw.Target.prototype.load = function () {
|
|||
* @returns {boolean} Saving has been started
|
||||
*/
|
||||
ve.init.mw.Target.prototype.save = function ( doc, options ) {
|
||||
var data, start;
|
||||
var data;
|
||||
// Prevent duplicate requests
|
||||
if ( this.saving ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = $.extend( {}, options, {
|
||||
'format': 'json',
|
||||
'action': 'visualeditoredit',
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid,
|
||||
'basetimestamp': this.baseTimeStamp,
|
||||
'starttimestamp': this.startTimeStamp,
|
||||
'html': this.getHtml( doc ),
|
||||
'token': this.editToken
|
||||
} );
|
||||
|
||||
// Save DOM
|
||||
start = ve.now();
|
||||
|
||||
this.saving = $.ajax( {
|
||||
'url': this.apiUrl,
|
||||
'data': data,
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000
|
||||
} )
|
||||
.then( function ( data, status, jqxhr ) {
|
||||
ve.track( 'performance.system.domSave', {
|
||||
'bytes': $.byteLength( jqxhr.responseText ),
|
||||
'duration': ve.now() - start,
|
||||
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
|
||||
} );
|
||||
return jqxhr;
|
||||
} )
|
||||
this.saving = this.tryWithPreparedCacheKey( doc, data, 'save' )
|
||||
.done( ve.bind( ve.init.mw.Target.onSave, this ) )
|
||||
.fail( ve.bind( ve.init.mw.Target.onSaveError, this ) );
|
||||
|
||||
|
@ -612,38 +751,26 @@ ve.init.mw.Target.prototype.save = function ( doc, options ) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Post DOM data to the Parsoid API to retreive wikitext diff.
|
||||
* Post DOM data to the Parsoid API to retrieve wikitext diff.
|
||||
*
|
||||
* @method
|
||||
* @param {HTMLDocument} doc Document to compare against (via wikitext)
|
||||
* @returns {boolean} Diffing has been started
|
||||
*/
|
||||
ve.init.mw.Target.prototype.showChanges = function ( doc ) {
|
||||
var start = ve.now();
|
||||
$.ajax( {
|
||||
'url': this.apiUrl,
|
||||
'data': {
|
||||
'format': 'json',
|
||||
'action': 'visualeditor',
|
||||
'paction': 'diff',
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid,
|
||||
'html': this.getHtml( doc )
|
||||
},
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000
|
||||
} )
|
||||
.then( function ( data, status, jqxhr ) {
|
||||
ve.track( 'performance.system.domDiff', {
|
||||
'bytes': $.byteLength( jqxhr.responseText ),
|
||||
'duration': ve.now() - start,
|
||||
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
|
||||
} );
|
||||
return jqxhr;
|
||||
} )
|
||||
if ( this.diffing ) {
|
||||
return false;
|
||||
}
|
||||
this.diffing = this.tryWithPreparedCacheKey( doc, {
|
||||
'action': 'visualeditor',
|
||||
'paction': 'diff',
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid,
|
||||
}, 'diff' )
|
||||
.done( ve.bind( ve.init.mw.Target.onShowChanges, this ) )
|
||||
.fail( ve.bind( ve.init.mw.Target.onShowChangesError, this ) );
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -706,41 +833,20 @@ ve.init.mw.Target.prototype.submit = function ( wikitext, options ) {
|
|||
* @method
|
||||
* @param {HTMLDocument} doc Document to serialize
|
||||
* @param {Function} callback Function to call when complete, accepts error and wikitext arguments
|
||||
* @returns {boolean} Serializing has beeen started
|
||||
* @returns {boolean} Serializing has been started
|
||||
*/
|
||||
ve.init.mw.Target.prototype.serialize = function ( doc, callback ) {
|
||||
var start = ve.now();
|
||||
// Prevent duplicate requests
|
||||
if ( this.serializing ) {
|
||||
return false;
|
||||
}
|
||||
// Load DOM
|
||||
this.serializing = true;
|
||||
this.serializeCallback = callback;
|
||||
$.ajax( {
|
||||
'url': this.apiUrl,
|
||||
'data': {
|
||||
'action': 'visualeditor',
|
||||
'paction': 'serialize',
|
||||
'html': this.getHtml( doc ),
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid,
|
||||
'format': 'json'
|
||||
},
|
||||
'dataType': 'json',
|
||||
'type': 'POST',
|
||||
// Wait up to 100 seconds before giving up
|
||||
'timeout': 100000,
|
||||
'cache': 'false'
|
||||
} )
|
||||
.then( function ( data, status, jqxhr ) {
|
||||
ve.track( 'performance.system.domSerialize', {
|
||||
'bytes': $.byteLength( jqxhr.responseText ),
|
||||
'duration': ve.now() - start,
|
||||
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
|
||||
} );
|
||||
return jqxhr;
|
||||
} )
|
||||
this.serializing = this.tryWithPreparedCacheKey( doc, {
|
||||
'action': 'visualeditor',
|
||||
'paction': 'serialize',
|
||||
'page': this.pageName,
|
||||
'oldid': this.revid
|
||||
}, 'serialize' )
|
||||
.done( ve.bind( ve.init.mw.Target.onSerialize, this ) )
|
||||
.fail( ve.bind( ve.init.mw.Target.onSerializeError, this ) );
|
||||
return true;
|
||||
|
|
Loading…
Reference in a new issue