jenkins-bot has submitted this change and it was merged. Change subject: Create APIResultsProvider and APIResultsQueue and add tests ......................................................................
Create APIResultsProvider and APIResultsQueue and add tests Generalize ResultsQueue and ResultsProvider downstream and create unit tests. Change-Id: I5346081317ef7195020b52deae322d70af80cae6 --- M .docs/categories.json M .docs/eg-iframe.html M build/modules.json M demos/ve/desktop.html M demos/ve/mobile.html A src/dm/ve.dm.APIResultsProvider.js A src/dm/ve.dm.APIResultsQueue.js A tests/dm/ve.dm.APIResultsQueue.test.js M tests/index.html 9 files changed, 629 insertions(+), 0 deletions(-) Approvals: Esanders: Looks good to me, approved jenkins-bot: Verified diff --git a/.docs/categories.json b/.docs/categories.json index b82b54c..da35277 100644 --- a/.docs/categories.json +++ b/.docs/categories.json @@ -41,6 +41,8 @@ "ve.dm.DocumentSynchronizer", "ve.dm.IndexValueStore", "ve.dm.Scalable", + "ve.dm.APIResultsProvider", + "ve.dm.APIResultsQueue", "ve.dm.NodeFactory", "ve.dm.Surface", "ve.dm.SurfaceFragment", diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html index 3159c0e..e0fe8ce 100644 --- a/.docs/eg-iframe.html +++ b/.docs/eg-iframe.html @@ -146,6 +146,8 @@ <script src="../src/dm/ve.dm.AlignableNode.js"></script> <script src="../src/dm/ve.dm.FocusableNode.js"></script> <script src="../src/dm/ve.dm.Scalable.js"></script> + <script src="../src/dm/ve.dm.APIResultsProvider.js"></script> + <script src="../src/dm/ve.dm.APIResultsQueue.js"></script> <script src="../src/dm/ve.dm.ResizableNode.js"></script> <script src="../src/dm/ve.dm.Node.js"></script> <script src="../src/dm/ve.dm.BranchNode.js"></script> diff --git a/build/modules.json b/build/modules.json index 981741b..f8bcaec 100644 --- a/build/modules.json +++ b/build/modules.json @@ -169,6 +169,8 @@ "src/dm/ve.dm.AlignableNode.js", "src/dm/ve.dm.FocusableNode.js", "src/dm/ve.dm.Scalable.js", + "src/dm/ve.dm.APIResultsProvider.js", + "src/dm/ve.dm.APIResultsQueue.js", "src/dm/ve.dm.ResizableNode.js", "src/dm/ve.dm.Node.js", "src/dm/ve.dm.BranchNode.js", @@ -451,6 +453,7 @@ "tests/dm/ve.dm.LinearData.test.js", "tests/dm/ve.dm.Transaction.test.js", "tests/dm/ve.dm.TransactionProcessor.test.js", + "tests/dm/ve.dm.APIResultsQueue.test.js", "tests/dm/ve.dm.Surface.test.js", "tests/dm/ve.dm.SurfaceFragment.test.js", "tests/dm/ve.dm.ModelRegistry.test.js", diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html index 8b356c9..a266c87 100644 --- a/demos/ve/desktop.html +++ b/demos/ve/desktop.html @@ -158,6 +158,8 @@ <script src="../../src/dm/ve.dm.AlignableNode.js"></script> <script src="../../src/dm/ve.dm.FocusableNode.js"></script> <script src="../../src/dm/ve.dm.Scalable.js"></script> + <script src="../../src/dm/ve.dm.APIResultsProvider.js"></script> + <script src="../../src/dm/ve.dm.APIResultsQueue.js"></script> <script src="../../src/dm/ve.dm.ResizableNode.js"></script> <script src="../../src/dm/ve.dm.Node.js"></script> <script src="../../src/dm/ve.dm.BranchNode.js"></script> diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html index a4fcf15..289f96c 100644 --- a/demos/ve/mobile.html +++ b/demos/ve/mobile.html @@ -159,6 +159,8 @@ <script src="../../src/dm/ve.dm.AlignableNode.js"></script> <script src="../../src/dm/ve.dm.FocusableNode.js"></script> <script src="../../src/dm/ve.dm.Scalable.js"></script> + <script src="../../src/dm/ve.dm.APIResultsProvider.js"></script> + <script src="../../src/dm/ve.dm.APIResultsQueue.js"></script> <script src="../../src/dm/ve.dm.ResizableNode.js"></script> <script src="../../src/dm/ve.dm.Node.js"></script> <script src="../../src/dm/ve.dm.BranchNode.js"></script> diff --git a/src/dm/ve.dm.APIResultsProvider.js b/src/dm/ve.dm.APIResultsProvider.js new file mode 100644 index 0000000..81e53c8 --- /dev/null +++ b/src/dm/ve.dm.APIResultsProvider.js @@ -0,0 +1,220 @@ +/*! + * VisualEditor DataModel ResourceProvider class. + * + * @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/** + * Resource Provider object. + * + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @param {string} apiurl The URL to the api + * @param {Object} [config] Configuration options + * @cfg {number} fetchLimit The default number of results to fetch + * @cfg {string} lang The language of the API + * @cfg {number} offset Initial offset, if relevant, to call results from + * @cfg {Object} ajaxSettings The settings for the ajax call + * @cfg {Object} staticParams The data parameters that are static and should + * always be sent to the API request, as opposed to user parameters. + * @cfg {Object} userParams Initial user parameters to be sent as data to + * the API request. These can change per request, like the search query term + * or sizing parameters for images, etc. + */ +ve.dm.APIResultsProvider = function VeDmResourceProvider( apiurl, config ) { + config = config || {}; + + this.setAPIurl( apiurl ); + this.fetchLimit = config.fetchLimit || 30; + this.lang = config.lang; + this.offset = config.offset || 0; + this.ajaxSettings = config.ajaxSettings || {}; + + this.staticParams = config.staticParams || {}; + this.userParams = config.userParams || {}; + + this.toggleDepleted( false ); + + // Mixin constructors + OO.EventEmitter.call( this ); +}; + +/* Setup */ +OO.mixinClass( ve.dm.APIResultsProvider, OO.EventEmitter ); + +/* Methods */ + +/** + * Get results from the source + * + * @param {number} howMany Number of results to ask for + * @return {jQuery.Promise} Promise that is resolved into an array + * of available results, or is rejected if no results are available. + */ +ve.dm.APIResultsProvider.prototype.getResults = function () { + var xhr, + deferred = $.Deferred(), + allParams = $.extend( {}, this.getStaticParams(), this.getUserParams() ); + + xhr = $.getJSON( this.getAPIurl(), allParams ) + .done( function ( data ) { + if ( + $.type( data ) !== 'array' || + ( + $.type( data ) === 'array' && + data.length === 0 + ) + ) { + deferred.resolve(); + } else { + deferred.resolve( data ); + } + } ); + return deferred.promise( { abort: xhr.abort } ); +}; + +/** + * Set API url + * + * @param {string} apiurl API url + */ +ve.dm.APIResultsProvider.prototype.setAPIurl = function ( apiurl ) { + this.apiurl = apiurl; +}; + +/** + * Set api url + * + * @returns {string} API url + */ +ve.dm.APIResultsProvider.prototype.getAPIurl = function () { + return this.apiurl; +}; + +/** + * Get the static, non-changing data parameters sent to the API + * + * @returns {Object} Data parameters + */ +ve.dm.APIResultsProvider.prototype.getStaticParams = function () { + return this.staticParams; +}; + +/** + * Get the user-inputted dybamic data parameters sent to the API + * + * @returns {Object} Data parameters + */ +ve.dm.APIResultsProvider.prototype.getUserParams = function () { + return this.userParams; +}; + +/** + * Set the data parameters sent to the API + * + * @param {Object} params User defined data parameters + */ +ve.dm.APIResultsProvider.prototype.setUserParams = function ( params ) { + // Assymetrically compare (params is subset of this.userParams) + if ( !ve.compare( params, this.userParams, true ) ) { + this.userParams = $.extend( {}, this.userParams, params ); + // Reset offset + this.setOffset( 0 ); + // Reset depleted status + this.toggleDepleted( false ); + } +}; + +/** + * Get fetch limit or 'page' size. This is the number + * of results per request. + * + * @returns {number} limit + */ +ve.dm.APIResultsProvider.prototype.getDefaultFetchLimit = function () { + return this.limit; +}; + +/** + * Set limit + * + * @param {number} limit Default number of results to fetch from the API + */ +ve.dm.APIResultsProvider.prototype.setDefaultFetchLimit = function ( limit ) { + this.limit = limit; +}; + +/** + * Get provider API language + * + * @returns {string} Provider API language + */ +ve.dm.APIResultsProvider.prototype.getLang = function () { + return this.lang; +}; + +/** + * Set provider API language + * + * @param {string} lang Provider API language + */ +ve.dm.APIResultsProvider.prototype.setLang = function ( lang ) { + this.lang = lang; +}; + +/** + * Get result offset + * + * @returns {number} Offset Results offset for the upcoming request + */ +ve.dm.APIResultsProvider.prototype.getOffset = function () { + return this.offset; +}; + +/** + * Set result offset + * + * @param {number} Results offset for the upcoming request + */ +ve.dm.APIResultsProvider.prototype.setOffset = function ( offset ) { + this.offset = offset; +}; + +/** + * Check whether the provider is depleted and has no more results + * to hand off. + * + * @returns {boolean} The provider is depleted + */ +ve.dm.APIResultsProvider.prototype.isDepleted = function () { + return this.depleted; +}; + +/** + * Toggle depleted state + * + * @param {boolean} isDepleted The provider is depleted + */ +ve.dm.APIResultsProvider.prototype.toggleDepleted = function ( isDepleted ) { + this.depleted = isDepleted !== undefined ? isDepleted : !this.depleted; +}; + +/** + * Get the default ajax settings + * + * @returns {Object} Ajax settings + */ +ve.dm.APIResultsProvider.prototype.getAjaxSettings = function () { + return this.ajaxSettings; +}; + +/** + * Get the default ajax settings + * + * @param {Object} settings Ajax settings + */ +ve.dm.APIResultsProvider.prototype.setAjaxSettings = function ( settings ) { + this.ajaxSettings = settings; +}; diff --git a/src/dm/ve.dm.APIResultsQueue.js b/src/dm/ve.dm.APIResultsQueue.js new file mode 100644 index 0000000..2e491fd --- /dev/null +++ b/src/dm/ve.dm.APIResultsQueue.js @@ -0,0 +1,204 @@ +/*! + * VisualEditor DataModel ResourceQueue class. + * + * @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org + */ + +/** + * Resource Queue object. + * + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} limit The default number of results to fetch + * @cfg {number} threshold The default number of extra results + * that the queue should always strive to have on top of the + * individual requests for items. + */ +ve.dm.APIResultsQueue = function VeDmResourceQueue( config ) { + config = config || {}; + + this.fileRepoPromise = null; + this.providers = []; + this.providerPromises = []; + this.queue = []; + + this.params = {}; + + this.limit = config.limit || 20; + this.setThreshold( config.threshold || 10 ); + + // Mixin constructors + OO.EventEmitter.call( this ); +}; + +/* Setup */ +OO.mixinClass( ve.dm.APIResultsQueue, OO.EventEmitter ); + +/* Methods */ + +/** + * Set up the queue and its resources. + * This should be overrided if there are any setup steps to perform. + * + * @return {jQuery.Promise} Promise that resolves when the resources + * are set up. Note: The promise must have an .abort() functionality. + */ +ve.dm.APIResultsQueue.prototype.setup = function () { + return $.Deferred().resolve().promise( { abort: $.noop } ); +}; + +/** + * Get items from the queue + * + * @param {number} [howMany] How many items to retrieve. Defaults to the + * default limit supplied on initialization. + * @return {jQuery.Promise} Promise that resolves into an array of items. + */ +ve.dm.APIResultsQueue.prototype.get = function ( howMany ) { + var fetchingPromise = null, + me = this; + + howMany = howMany || this.limit; + + // Check if the queue has enough items + if ( this.queue.length < howMany + this.threshold ) { + // Call for more results + fetchingPromise = this.queryProviders( howMany + this.threshold ) + .then( function ( items ) { + // Add to the queue + me.queue = me.queue.concat.apply( me.queue, items ); + } ); + } + + return $.when( fetchingPromise ) + .then( function () { + return me.queue.splice( 0, howMany ); + } ); + +}; + +/** + * Get results from all providers + * + * @param {number} [howMany] How many items to retrieve. Defaults to the + * default limit supplied on initialization. + * @return {jQuery.Promise} Promise that is resolved into an array + * of fetched items. Note: The promise must have an .abort() functionality. + */ +ve.dm.APIResultsQueue.prototype.queryProviders = function ( howMany ) { + var i, len, + queue = this; + + // Make sure there are resources set up + return this.setup() + .then( function () { + // Abort previous requests + for ( i = 0, len = queue.providerPromises.length; i < len; i++ ) { + queue.providerPromises[i].abort(); + } + queue.providerPromises = []; + // Set up the query to all providers + for ( i = 0, len = queue.providers.length; i < len; i++ ) { + if ( !queue.providers[i].isDepleted() ) { + queue.providerPromises.push( + queue.providers[i].getResults( howMany ) + ); + } + } + + return $.when.apply( $, queue.providerPromises ) + .then( Array.prototype.concat.bind( [] ) ); + } ); +}; + +/** + * Set the search query for all the providers. + * + * This also makes sure to abort any previous promises. + * + * @param {Object} params API search parameters + */ +ve.dm.APIResultsQueue.prototype.setParams = function ( params ) { + var i, len; + if ( !ve.compare( params, this.params, true ) ) { + this.params = ve.extendObject( this.params, params ); + // Reset queue + this.queue = []; + // Reset promises + for ( i = 0, len = this.providerPromises.length; i < len; i++ ) { + this.providerPromises[i].abort(); + } + // Change queries + for ( i = 0, len = this.providers.length; i < len; i++ ) { + this.providers[i].setUserParams( this.params ); + } + } +}; + +/** + * Get the data parameters sent to the API + * + * @returns {Object} params API search parameters + */ +ve.dm.APIResultsQueue.prototype.getParams = function () { + return this.params; +}; + +/** + * Set the providers + * + * @param {ve.dm.APIResultsProvider[]} providers An array of providers + */ +ve.dm.APIResultsQueue.prototype.setProviders = function ( providers ) { + this.providers = providers; +}; + +/** + * Add a provbider to the group + * + * @param {ve.dm.APIResultsProvider} provider A provider object + */ +ve.dm.APIResultsQueue.prototype.addProvider = function ( provider ) { + this.providers.push( provider ); +}; + +/** + * Set the providers + * + * @returns {ve.dm.APIResultsProvider[]} providers An array of providers + */ +ve.dm.APIResultsQueue.prototype.getProviders = function () { + return this.providers; +}; + +/** + * Get the queue size + * + * @return {number} Queue size + */ +ve.dm.APIResultsQueue.prototype.getQueueSize = function () { + return this.queue.length; +}; + +/** + * Set queue threshold + * + * @param {number} threshold Queue threshold, below which we will + * request more items + */ +ve.dm.APIResultsQueue.prototype.setThreshold = function ( threshold ) { + this.threshold = threshold; +}; + +/** + * Get queue threshold + * + * @returns {number} threshold Queue threshold, below which we will + * request more items + */ +ve.dm.APIResultsQueue.prototype.getThreshold = function () { + return this.threshold; +}; diff --git a/tests/dm/ve.dm.APIResultsQueue.test.js b/tests/dm/ve.dm.APIResultsQueue.test.js new file mode 100644 index 0000000..4a185e0 --- /dev/null +++ b/tests/dm/ve.dm.APIResultsQueue.test.js @@ -0,0 +1,191 @@ +/*! + * VisualEditor DataModel ResourceQueue tests. + * + * @copyright 2011-2015 VisualEditor Team and others; see http://ve.mit-license.org + */ + +QUnit.module( 've.dm.APIResultsQueue' ); + +var itemCounter = 0, + responseDelay = 1000, + FullResourceProvider = function VeDmFullResourceProvider( config ) { + this.timer = null; + // Inheritance + ve.dm.APIResultsProvider.call( this, '', config ); + }, + EmptyResourceProvider = function VeDmEmptyResourceProvider( config ) { + this.timer = null; + // Inheritance + ve.dm.APIResultsProvider.call( this, '', config ); + }, + SingleResultResourceProvider = function VeDmSingleResultResourceProvider( config ) { + this.timer = null; + // Inheritance + ve.dm.APIResultsProvider.call( this, '', config ); + }; + +OO.inheritClass( FullResourceProvider, ve.dm.APIResultsProvider ); +OO.inheritClass( EmptyResourceProvider, ve.dm.APIResultsProvider ); +OO.inheritClass( SingleResultResourceProvider, ve.dm.APIResultsProvider ); + +FullResourceProvider.prototype.getResults = function ( howMany ) { + var i, timer, + result = [], + deferred = $.Deferred(); + + for ( i = itemCounter; i < itemCounter + howMany; i++ ) { + result.push( 'result ' + ( i + 1 ) ); + } + itemCounter = i; + + timer = setTimeout( + function () { + // Always resolve with some values + deferred.resolve( result ); + }, + responseDelay ); + + return deferred.promise( { abort: function () { clearTimeout( timer ); } } ); +}; + +EmptyResourceProvider.prototype.getResults = function () { + var me = this, + deferred = $.Deferred(), + timer = setTimeout( + function () { + me.toggleDepleted( true ); + // Always resolve with empty value + deferred.resolve( [] ); + }, + responseDelay ); + + return deferred.promise( { abort: function () { clearTimeout( timer ); } } ); +}; + +SingleResultResourceProvider.prototype.getResults = function ( howMany ) { + var timer, + me = this, + deferred = $.Deferred(); + + timer = setTimeout( + function () { + me.toggleDepleted( howMany > 1 ); + // Always resolve with one value + deferred.resolve( [ 'one result (' + ( itemCounter++ + 1 ) + ')' ] ); + }, + responseDelay ); + + return deferred.promise( { abort: function () { clearTimeout( timer ); } } ); +}; + +/* Tests */ + +QUnit.test( 'Query providers', function ( assert ) { + var done = assert.async(), + providers = [ + new FullResourceProvider(), + new EmptyResourceProvider(), + new SingleResultResourceProvider() + ], + queue = new ve.dm.APIResultsQueue( { + threshold: 2 + } ); + + assert.expect( 15 ); + + // Add providers to queue + queue.setProviders( providers ); + + // Set parameters and fetch + queue.setParams( { foo: 'bar' } ); + + queue.get( 10 ) + .then( function ( data ) { + // Check that we received all requested results + assert.equal( data.length, 10, 'Query 1: Results received.' ); + // We've asked for 10 items + 2 threshold from all providers. + // Provider 1 returned 12 results + // Provider 2 returned 0 results + // Provider 3 returned 1 results + // Overall 13 results. 10 were retrieved. 3 left in queue. + assert.equal( queue.getQueueSize(), 3, 'Query 1: Remaining queue size.' ); + + // Check if sources are depleted + assert.ok( !providers[ 0 ].isDepleted(), 'Query 1: Full provider not depleted.' ); + assert.ok( providers[ 1 ].isDepleted(), 'Query 1: Empty provider is depleted.' ); + assert.ok( providers[ 2 ].isDepleted(), 'Query 1: Single result provider is depleted.' ); + + // Ask for more results + return queue.get( 10 ); + } ) + .then( function ( data1 ) { + // This time, only provider 1 was queried, because the other + // two were marked as depleted. + // * We asked for 10 items + // * There are currently 3 items in the queue + // * The queue queried provider #1 for 12 items + // * The queue returned 10 results as requested + // * 5 results are now left in the queue. + assert.equal( data1.length, 10, 'Query 1: Second set of results received.' ); + assert.equal( queue.getQueueSize(), 5, 'Query 1: Remaining queue size.' ); + + // Change the query + queue.setParams( { foo: 'baz' } ); + // Check if sources are depleted + assert.ok( !providers[ 0 ].isDepleted(), 'Query 2: Full provider not depleted.' ); + assert.ok( !providers[ 1 ].isDepleted(), 'Query 2: Empty provider not depleted.' ); + assert.ok( !providers[ 2 ].isDepleted(), 'Query 2: Single result provider not depleted.' ); + + return queue.get( 10 ); + } ) + .then( function ( data2 ) { + // This should be the same as the very first result + assert.equal( data2.length, 10, 'Query 2: Results received.' ); + assert.equal( queue.getQueueSize(), 3, 'Query 2: Remaining queue size.' ); + // Check if sources are depleted + assert.ok( !providers[ 0 ].isDepleted(), 'Query 2: Full provider not depleted.' ); + assert.ok( providers[ 1 ].isDepleted(), 'Query 2: Empty provider is not depleted.' ); + assert.ok( providers[ 2 ].isDepleted(), 'Query 2: Single result provider is not depleted.' ); + } ) + // Finish the async test + .then( done ); +} ); + +QUnit.test( 'Abort providers', function ( assert ) { + var done = assert.async(), + completed = false, + biggerQueue = new ve.dm.APIResultsQueue( { + threshold: 5 + } ), + providers2 = [ + new FullResourceProvider(), + new EmptyResourceProvider(), + new SingleResultResourceProvider() + ]; + + assert.expect( 1 ); + + // Make the delay higher + responseDelay = 3000; + + // Add providers to queue + biggerQueue.setProviders( providers2 ); + + biggerQueue.setParams( { foo: 'bar' } ); + biggerQueue.get( 100 ) + .always( function () { + // This should only run if the promise wasn't aborted + completed = true; + } ); + + // Make the delay higher + responseDelay = 5000; + + biggerQueue.setParams( { foo: 'baz' } ); + biggerQueue.get( 10 ) + .then( function () { + assert.ok( !completed, 'Provider promises aborted.' ); + } ) + // Finish the async test + .then( done ); +} ); diff --git a/tests/index.html b/tests/index.html index 43bd38c..5e2f0ea 100644 --- a/tests/index.html +++ b/tests/index.html @@ -101,6 +101,8 @@ <script src="../src/dm/ve.dm.AlignableNode.js"></script> <script src="../src/dm/ve.dm.FocusableNode.js"></script> <script src="../src/dm/ve.dm.Scalable.js"></script> + <script src="../src/dm/ve.dm.APIResultsProvider.js"></script> + <script src="../src/dm/ve.dm.APIResultsQueue.js"></script> <script src="../src/dm/ve.dm.ResizableNode.js"></script> <script src="../src/dm/ve.dm.Node.js"></script> <script src="../src/dm/ve.dm.BranchNode.js"></script> @@ -345,6 +347,7 @@ <script src="../tests/dm/ve.dm.LinearData.test.js"></script> <script src="../tests/dm/ve.dm.Transaction.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> <script src="../tests/dm/ve.dm.SurfaceFragment.test.js"></script> <script src="../tests/dm/ve.dm.ModelRegistry.test.js"></script> -- To view, visit https://gerrit.wikimedia.org/r/189627 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I5346081317ef7195020b52deae322d70af80cae6 Gerrit-PatchSet: 15 Gerrit-Project: VisualEditor/VisualEditor Gerrit-Branch: master Gerrit-Owner: Mooeypoo <mor...@gmail.com> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Krinkle <krinklem...@gmail.com> Gerrit-Reviewer: Mooeypoo <mor...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits