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 f342bac30..4635e9142 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 ); @@ -147,13 +148,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' ); } ); @@ -177,12 +178,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 f8230db3a..44e32bcfe 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 );