Combine imageinfo requests

By generalising LinkCache into ApiBatchQueue to request them all centrally

Bug: T75822
Change-Id: I097311ec2487bb1ae9f5d927c2c13c274ba716f9
This commit is contained in:
Alex Monk 2014-12-16 01:06:05 +00:00
parent fca9110172
commit 7876cc957a
6 changed files with 247 additions and 160 deletions

View file

@ -298,7 +298,9 @@ $wgResourceModules += array(
'scripts' => array(
// init
'modules/ve-mw/init/ve.init.mw.js',
'modules/ve-mw/init/ve.init.mw.ApiResponseCache.js',
'modules/ve-mw/init/ve.init.mw.LinkCache.js',
'modules/ve-mw/init/ve.init.mw.ImageInfoCache.js',
'modules/ve-mw/init/ve.init.mw.Platform.js',
'modules/ve-mw/init/ve.init.mw.Target.js',
'modules/ve-mw/init/ve.init.mw.TargetEvents.js',

View file

@ -164,36 +164,18 @@ ve.dm.MWImageNode.static.syncScalableToType = function ( type, mediaType, scalab
* @returns {jQuery.Promise} Promise which resolves after the image size details are fetched from the API
*/
ve.dm.MWImageNode.static.getScalablePromise = function ( filename ) {
var scalablePromise = $.Deferred();
// On the first call set off an async call to update the scalable's
// original dimensions from the API.
if ( ve.init.target ) {
ve.init.target.constructor.static.apiRequest(
{
action: 'query',
prop: 'imageinfo',
indexpageids: '1',
iiprop: 'size|mediatype',
titles: filename
},
{ type: 'POST' }
)
.done( function ( response ) {
var page = response.query && response.query.pages[response.query.pageids[0]],
info = page && page.imageinfo && page.imageinfo[0];
if ( info ) {
scalablePromise.resolve( info );
} else {
scalablePromise.reject();
if ( ve.init.platform.imageInfoCache ) {
return ve.init.platform.imageInfoCache.get( filename ).then( function ( info ) {
if ( !info ) {
return $.Deferred().reject().promise();
}
} )
.fail( function () {
scalablePromise.reject();
return info;
} );
} else {
scalablePromise.reject();
return $.Deferred().reject().promise();
}
return scalablePromise;
};
/* Methods */
@ -248,8 +230,10 @@ ve.dm.MWImageNode.prototype.getSizeHash = function () {
ve.dm.MWImageNode.prototype.getScalable = function () {
var imageNode = this;
if ( !this.scalablePromise ) {
this.scalablePromise = ve.dm.MWImageNode.static.getScalablePromise( this.getFilename() )
.done( function ( info ) {
this.scalablePromise = ve.dm.MWImageNode.static.getScalablePromise( this.getFilename() );
// If the promise was already resolved before getScalablePromise returned, then jQuery will execute the done straight away.
// So don't just do getScalablePromise( ... ).done because we need to make sure that this.scalablePromise gets set first.
this.scalablePromise.done( function ( info ) {
if ( info ) {
imageNode.getScalable().setOriginalDimensions( {
width: info.width,

View file

@ -0,0 +1,172 @@
/*!
* VisualEditor MediaWiki Initialization ApiResponseCache class.
*
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
var hasOwn = Object.prototype.hasOwnProperty;
/**
* MediaWiki API batch queue.
*
* Used to queue up lists of items centrally to get information about in batches
* of requests.
*
* @class
* @extends OO.EventEmitter
* @constructor
*/
ve.init.mw.ApiResponseCache = function VeInitMwApiResponseCache() {
// Mixin constructor
OO.EventEmitter.call( this );
// Keys are titles, values are deferreds
this.deferreds = {};
// Keys are page names, values are link data objects
// This is kept for synchronous retrieval of cached values via #getCached
this.cacheValues = {};
// Array of page titles queued to be looked up
this.queue = [];
this.schedule = ve.debounce( this.processQueue.bind( this ), 0 );
};
OO.mixinClass( ve.init.mw.ApiResponseCache, OO.EventEmitter );
// TODO should reuse ve.dm.MWInternalLinkAnnotation#getLookupTitle once factored out
ve.init.mw.ApiResponseCache.prototype.normalizeTitle = function ( title ) {
var titleObj = mw.Title.newFromText( title );
if ( !titleObj ) {
return title;
}
return titleObj.getPrefixedText();
};
/**
* Look up data about a title. If the data about this title is already in the cache, this
* returns an already-resolved promise. Otherwise, it returns a pending promise and schedules
* an request to retrieve the data.
* @param {string} title Title
* @returns {jQuery.Promise} Promise that will be resolved with the data once it's available
*/
ve.init.mw.ApiResponseCache.prototype.get = function ( title ) {
if ( typeof title !== 'string' ) {
// Don't bother letting things like undefined or null make it all the way through,
// just reject them here. Otherwise they'll cause problems or exceptions at random
// other points in this file.
return $.Deferred().reject().promise();
}
title = this.normalizeTitle( title );
if ( !hasOwn.call( this.deferreds, title ) ) {
this.deferreds[title] = $.Deferred();
this.queue.push( title );
this.schedule();
}
return this.deferreds[title].promise();
};
/**
* Look up data about a page in the cache. If the data about this page is already in the cache,
* this returns that data. Otherwise, it returns undefined.
*
* @param {string} name Normalized page title
* @returns {Object|undefined} Cache data for this name.
*/
ve.init.mw.ApiResponseCache.prototype.getCached = function ( name ) {
if ( hasOwn.call( this.cacheValues, name ) ) {
return this.cacheValues[name];
}
};
/**
* Fired when a new entry is added to the cache.
* @event add
* @param {Object} entries Cache entries that were added. Object mapping names to data objects.
*/
/**
* Add entries to the cache.
* @param {Object} entries Object keyed by page title, with the values being data objects
* @fires add
*/
ve.init.mw.ApiResponseCache.prototype.set = function ( entries ) {
var name;
for ( name in entries ) {
if ( hasOwn.call( this.cacheValues, name ) ) {
continue;
}
if ( !hasOwn.call( this.deferreds, name ) ) {
this.deferreds[name] = $.Deferred();
}
this.deferreds[name].resolve( entries[name] );
this.cacheValues[name] = entries[name];
}
this.emit( 'add', ve.getObjectKeys( entries ) );
};
/**
* Get an API request promise to deal with a list of titles
* @abstract
* @param subqueue
* @return {jQuery.Promise}
*/
ve.init.mw.ApiResponseCache.prototype.getRequestPromise = function () {
throw new Error( 'Stub, override in subclass' );
};
/**
* Process each page in the response of an API request
* @abstract
* @param {Object} page The page object
* @return {Object} The relevant info that we want to cache and return.
*/
ve.init.mw.ApiResponseCache.prototype.processPage = function () {
throw new Error( 'Stub, override in subclass' );
};
/**
* Perform any scheduled API requests.
* @private
* @fires add
*/
ve.init.mw.ApiResponseCache.prototype.processQueue = function () {
var subqueue, queue, batchQueue = this;
function rejectSubqueue( rejectQueue ) {
var i, len;
for ( i = 0, len = rejectQueue.length; i < len; i++ ) {
batchQueue.deferreds[rejectQueue[i]].reject();
}
}
function processResult( data ) {
var pageid, page,
pages = ( data.query && data.query.pages ) || data.pages,
processed = {};
if ( pages ) {
for ( pageid in pages ) {
page = pages[pageid];
processed[page.title] = batchQueue.processPage( page );
}
batchQueue.set( processed );
}
}
queue = this.queue;
this.queue = [];
while ( queue.length ) {
subqueue = queue.splice( 0, 50 ).map( this.normalizeTitle );
this.getRequestPromise( subqueue )
.then( processResult )
// Reject everything in subqueue; this will only reject the ones
// that weren't already resolved above, because .reject() on an
// already resolved Deferred is a no-op.
.then( rejectSubqueue.bind( null, subqueue ) );
}
};
} () );

View file

@ -0,0 +1,42 @@
/*!
* VisualEditor MediaWiki Initialization ImageInfoCache class.
*
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
/**
* Get information about images.
* @class
* @extends ve.init.mw.ApiResponseCache
* @constructor
*/
ve.init.mw.ImageInfoCache = function VeInitMwImageInfoCache() {
ve.init.mw.ImageInfoCache.super.call( this );
};
OO.inheritClass( ve.init.mw.ImageInfoCache, ve.init.mw.ApiResponseCache );
/**
* @inheritdoc
*/
ve.init.mw.ImageInfoCache.prototype.getRequestPromise = function ( subqueue ) {
return ve.init.target.constructor.static.apiRequest(
{
action: 'query',
prop: 'imageinfo',
indexpageids: '1',
iiprop: 'size|mediatype',
titles: subqueue.join( '|' )
},
{ type: 'POST' }
);
};
/**
* @inheritdoc
*/
ve.init.mw.ImageInfoCache.prototype.processPage = function ( page ) {
return page.imageinfo[0];
};
} () );

View file

@ -5,90 +5,24 @@
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
var hasOwn = Object.prototype.hasOwnProperty;
// TODO should reuse ve.dm.MWInternalLinkAnnotation#getLookupTitle once factored out
function normalizeTitle( name ) {
var title = mw.Title.newFromText( name );
if ( !title ) {
return name;
}
return title.getPrefixedText();
}
/**
* MediaWiki link status cache.
*
* Used to track how to style links to a given page based on their existence status etc.
*
* Caches information about titles.
* @class
*
* @extends ve.init.mw.ApiResponseCache
* @constructor
*/
ve.init.mw.LinkCache = function VeInitMwLinkCache() {
// Mixin constructor
OO.EventEmitter.call( this );
// Keys are page names, values are deferreds
this.cache = {};
ve.init.mw.LinkCache.super.call( this );
// Keys are page names, values are link data objects
// This is kept for synchronous retrieval of cached values via #getCached
this.cacheValues = {};
// Array of page names queued to be looked up
this.queue = [];
this.schedule = ve.debounce( this.processQueue.bind( this ), 0 );
};
OO.mixinClass( ve.init.mw.LinkCache, OO.EventEmitter );
OO.inheritClass( ve.init.mw.LinkCache, ve.init.mw.ApiResponseCache );
// TODO maybe factor out generic parts into ve.StatusCache or whatever
// TODO unit tests
/**
* Fired when a new entry is added to the cache.
* @event add
* @param {Object} entries Cache entries that were added. Object mapping names to data objects.
*/
/**
* Look up data about a page in the cache. If the data about this page is already in the cache,
* this returns an already-resolved promise. Otherwise, it returns a pending promise and schedules
* an API call to retrieve the data.
*
* @param {string} name Normalized page title
* @returns {jQuery.Promise} Promise that will be resolved with the data once it's available
*/
ve.init.mw.LinkCache.prototype.get = function ( name ) {
if ( typeof name !== 'string' ) {
// Don't bother letting things like undefined or null make it all the way through,
// just reject them here. Otherwise they'll cause problems or exceptions at random
// other points in this file.
return $.Deferred().reject().promise();
}
if ( !hasOwn.call( this.cache, name ) ) {
this.cache[name] = $.Deferred();
this.queue.push( name );
this.schedule();
}
return this.cache[name].promise();
};
/**
* Look up data about a page in the cache. If the data about this page is already in the cache,
* this returns that data. Otherwise, it returns undefined.
*
* @param {string} name Normalized page title
* @returns {Object|undefined} Cache data for this name.
*/
ve.init.mw.LinkCache.prototype.getCached = function ( name ) {
if ( hasOwn.call( this.cacheValues, name ) ) {
return this.cacheValues[name];
}
};
/**
* Requests information about the title, then adds classes to the provided element as appropriate.
*
@ -114,74 +48,26 @@
};
/**
* Add entries to the cache.
* @param {Object} entries Object keyed by page title, with the values being data objects
* @fires add
* @inheritdoc
*/
ve.init.mw.LinkCache.prototype.set = function ( entries ) {
var name;
for ( name in entries ) {
if ( !hasOwn.call( this.cache, name ) ) {
this.cache[name] = $.Deferred();
}
this.cache[name].resolve( entries[name] );
this.cacheValues[name] = entries[name];
}
this.emit( 'add', ve.getObjectKeys( entries ) );
ve.init.mw.LinkCache.prototype.getRequestPromise = function ( subqueue ) {
return ve.init.target.constructor.static.apiRequest( {
action: 'query',
prop: 'info|pageprops',
ppprop: 'disambiguation',
titles: subqueue.join( '|' )
} );
};
/**
* Perform any scheduled API requests.
* @private
* @fires add
* @inheritdoc
*/
ve.init.mw.LinkCache.prototype.processQueue = function () {
var subqueue, queue, linkCache = this;
function rejectSubqueue() {
var i, len;
for ( i = 0, len = subqueue.length; i < len; i++ ) {
linkCache.cache[subqueue[i]].reject();
}
}
function processData( data ) {
var pageid, page, info,
pages = data.query && data.query.pages,
processed = {};
if ( pages ) {
for ( pageid in pages ) {
page = pages[pageid];
info = {
missing: page.missing !== undefined,
redirect: page.redirect !== undefined,
// Disambiguator extension
disambiguation: page.pageprops && page.pageprops.disambiguation !== undefined
};
processed[page.title] = info;
}
linkCache.set( processed );
}
// Reject everything in subqueue; this will only reject the ones
// that weren't already resolved above, because .reject() on an
// already resolved Deferred is a no-op.
rejectSubqueue();
}
queue = this.queue;
this.queue = [];
while ( queue.length ) {
subqueue = queue.splice( 0, 50 ).map( normalizeTitle );
ve.init.target.constructor.static.apiRequest( {
action: 'query',
prop: 'info|pageprops',
ppprop: 'disambiguation',
titles: subqueue.join( '|' )
} )
.done( processData )
.fail( rejectSubqueue );
}
ve.init.mw.LinkCache.prototype.processPage = function ( page ) {
return {
missing: page.missing !== undefined,
redirect: page.redirect !== undefined,
// Disambiguator extension
disambiguation: page.pageprops && page.pageprops.disambiguation !== undefined
};
};
} () );

View file

@ -23,6 +23,7 @@ ve.init.mw.Platform = function VeInitMwPlatform() {
);
this.parsedMessages = {};
this.linkCache = new ve.init.mw.LinkCache();
this.imageInfoCache = new ve.init.mw.ImageInfoCache();
};
/* Inheritance */