From a1ec29d06a89aa8cafeda7820a6c413ffeb005b4 Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Wed, 22 Jun 2016 11:45:58 -0700 Subject: [PATCH] Add a PromisePrioritizer and use it for notifications fetching This will allow us to let the user click filters quickly, effectively changing the promises sent to the API, but let the API only resolve with the latest requested promise. Bug: T136895 Change-Id: I698a2b8eced6d8ee997efef353697d27d92cfb2f --- Resources.php | 1 + modules/api/mw.echo.api.EchoApi.js | 9 +- modules/api/mw.echo.api.PromisePrioritizer.js | 88 +++++++++++++++++++ .../ui/mw.echo.ui.NotificationsInboxWidget.js | 2 - 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 modules/api/mw.echo.api.PromisePrioritizer.js diff --git a/Resources.php b/Resources.php index 84bf1b0dc..0f0e7cbb7 100644 --- a/Resources.php +++ b/Resources.php @@ -196,6 +196,7 @@ $wgResourceModules += array( 'ext.echo.api' => $echoResourceTemplate + array( 'scripts' => array( 'api/mw.echo.api.js', + 'api/mw.echo.api.PromisePrioritizer.js', 'api/mw.echo.api.EchoApi.js', 'api/mw.echo.api.APIHandler.js', 'api/mw.echo.api.LocalAPIHandler.js', diff --git a/modules/api/mw.echo.api.EchoApi.js b/modules/api/mw.echo.api.EchoApi.js index a37a0b73d..f481189fc 100644 --- a/modules/api/mw.echo.api.EchoApi.js +++ b/modules/api/mw.echo.api.EchoApi.js @@ -15,6 +15,7 @@ this.fetchingPromise = null; this.limit = config.limit || 25; + this.fetchingPrioritizer = new mw.echo.api.PromisePrioritizer(); }; OO.initClass( mw.echo.api.EchoApi ); @@ -142,13 +143,13 @@ return $.Deferred().reject().promise(); } - return handler.fetchNotifications( + return this.fetchingPrioritizer.prioritize( handler.fetchNotifications( type, // For the remote source, we are fetching 'local' notifications 'local', !!isForced, this.convertFiltersToAPIParams( filters ) - ) + ) ) .then( function ( result ) { return OO.getProp( result.query, 'notifications' ); } ); @@ -172,12 +173,12 @@ [ sources ] : 'local'; - return this.network.getApiHandler( 'local' ).fetchNotifications( + return this.fetchingPrioritizer.prioritize( this.network.getApiHandler( 'local' ).fetchNotifications( type, sources, isForced, this.convertFiltersToAPIParams( filters ) - ) + ) ) .then( function ( result ) { return OO.getProp( result.query, 'notifications' ); } ); diff --git a/modules/api/mw.echo.api.PromisePrioritizer.js b/modules/api/mw.echo.api.PromisePrioritizer.js new file mode 100644 index 000000000..c1db40d3c --- /dev/null +++ b/modules/api/mw.echo.api.PromisePrioritizer.js @@ -0,0 +1,88 @@ +( function ( mw, $ ) { + /** + * Promise prioritizer for API actions. The prioritizer takes + * a promise at a time, always prioritizing the latest promise and + * aborting and ignoring the others. + * + * This allows us to send multiple promises in quick successions but + * trust that we get back only the latest successful request. + * + * @class + * + * @constructor + */ + mw.echo.api.PromisePrioritizer = function MwEchoApiPromisePrioritizer() { + this.deferred = $.Deferred(); + this.promise = null; + }; + + /* Initialization */ + + OO.initClass( mw.echo.api.PromisePrioritizer ); + + /** + * Prioritize a promise + * + * @param {jQuery.Promise|Promise} promise Promise + * @return {jQuery.Promise} The main deferred object that resolves + * or rejects when the latest promise is resolved or rejected. + */ + mw.echo.api.PromisePrioritizer.prototype.prioritize = function ( promise ) { + var previousPromise = this.promise; + + promise + .then( + this.setSuccess.bind( this, promise ), + this.setFailure.bind( this, promise ) + ); + this.promise = promise; + + if ( previousPromise && previousPromise.abort ) { + previousPromise.abort(); + } + + return this.deferred.promise(); + }; + + /** + * Set success for the promise. Resolve the main deferred object only + * if we are dealing with the currently prioritized promise. + * + * @param {jQuery.Promise} promise The promise that resolved successfully. + * The main deferred object is resolved with the result of the + * latest prioritized promise. + */ + mw.echo.api.PromisePrioritizer.prototype.setSuccess = function ( promise ) { + var prioritizer = this; + + if ( this.promise === promise ) { + this.promise.done( function () { + prioritizer.deferred.resolve.apply( prioritizer.deferred, arguments ); + + prioritizer.promise = null; + prioritizer.deferred = $.Deferred(); + } ); + } + }; + + /** + * Set failure for the promise. Reject the main deferred object only + * if we are dealing with the currently prioritized promise. + * + * @param {jQuery.Promise} promise The promise that failed. + * The main deferred object is rejected with the result of the + * latest prioritized promise + */ + mw.echo.api.PromisePrioritizer.prototype.setFailure = function ( promise ) { + var prioritizer = this; + + if ( this.promise === promise ) { + this.promise.fail( function () { + prioritizer.deferred.reject.apply( prioritizer.deferred, arguments ); + + prioritizer.promise = null; + prioritizer.deferred = $.Deferred(); + } ); + } + }; +} )( mediaWiki, jQuery ); diff --git a/modules/ui/mw.echo.ui.NotificationsInboxWidget.js b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js index a36d9d495..18c6717ca 100644 --- a/modules/ui/mw.echo.ui.NotificationsInboxWidget.js +++ b/modules/ui/mw.echo.ui.NotificationsInboxWidget.js @@ -205,7 +205,6 @@ */ mw.echo.ui.NotificationsInboxWidget.prototype.pushPending = function () { this.noticeMessageWidget.toggle( false ); - this.readStateSelectWidget.setDisabled( true ); this.topPaginationWidget.setDisabled( true ); this.bottomPaginationWidget.setDisabled( true ); @@ -218,7 +217,6 @@ */ mw.echo.ui.NotificationsInboxWidget.prototype.popPending = function () { this.resetMessageLabel(); - this.readStateSelectWidget.setDisabled( false ); this.topPaginationWidget.setDisabled( false ); this.bottomPaginationWidget.setDisabled( false );