mediawiki.page.image.pagination: Cache last-loaded pages
authorBartosz Dziewoński <matma.rex@gmail.com>
Wed, 30 Jul 2014 17:54:38 +0000 (19:54 +0200)
committerBartosz Dziewoński <matma.rex@gmail.com>
Sun, 28 Sep 2014 16:12:47 +0000 (16:12 +0000)
I often find myself switching back-and-forth between the few pages I
last looked at. Let's cache their information to avoid downloading it
again and again.

I've only implemented a count-based limit on the queue (no more than
10 entries) and no time-based limit (entries never expire). I don't
think one is necessary, but that might be due to my laziness…

Change-Id: I5873946e3e3b37a8951c1ac84e3e1a206b9300d1

resources/src/mediawiki.page/mediawiki.page.image.pagination.js

index 622e818..26c32a5 100644 (file)
@@ -2,23 +2,65 @@
  * Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
  */
 ( function ( mw, $ ) {
-       var jqXhr, $multipageimage, $spinner;
+       var jqXhr, $multipageimage, $spinner,
+               cache = {}, cacheOrder = [];
 
-       /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+       /* Fetch the next page, caching up to 10 last-loaded pages.
         * @param {string} url
-        * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
-        *   true, this function won't push a new history state, for the browser did so already).
+        * @return {jQuery.Promise}
         */
-       function loadPage( url, hist ) {
-               var $tr;
-               if ( jqXhr ) {
+       function fetchPageData( url ) {
+               if ( jqXhr && jqXhr.abort ) {
                        // Prevent race conditions and piling up pending requests
                        jqXhr.abort();
-                       jqXhr = undefined;
                }
+               jqXhr = undefined;
+
+               // Try the cache
+               if ( cache[url] ) {
+                       // Update access freshness
+                       cacheOrder.splice( $.inArray( url, cacheOrder ), 1 );
+                       cacheOrder.push( url );
+                       return $.Deferred().resolve( cache[url] ).promise();
+               }
+
+               // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
+               // (thumbnail urls) and update the interface manually.
+               jqXhr = $.ajax( url ).then( function ( data ) {
+                       return $( data ).find( 'table.multipageimage' ).contents();
+               } );
 
-               // Add a new spinner if one doesn't already exist
-               if ( !$spinner ) {
+               // Handle cache updates
+               jqXhr.done( function ( $contents ) {
+                       jqXhr = undefined;
+
+                       // Cache the newly loaded page
+                       cache[url] = $contents;
+                       cacheOrder.push( url );
+
+                       // Remove the oldest entry if we're over the limit
+                       if ( cacheOrder.length > 10 ) {
+                               delete cache[ cacheOrder[0] ];
+                               cacheOrder = cacheOrder.slice( 1 );
+                       }
+               } );
+
+               return jqXhr.promise();
+       }
+
+       /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+        * @param {string} url
+        * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
+        *   true, this function won't push a new history state, for the browser did so already).
+        */
+       function switchPage( url, hist ) {
+               var $tr, promise;
+
+               // Start fetching data (might be cached)
+               promise = fetchPageData( url );
+
+               // Add a new spinner if one doesn't already exist and the data is not already ready
+               if ( !$spinner && promise.state() !== 'resolved' ) {
                        $tr = $multipageimage.find( 'tr' );
                        $spinner = $.createSpinner( {
                                size: 'large',
                        $multipageimage.empty().append( $spinner );
                }
 
-               // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
-               // (thumbnail urls) and update the interface manually.
-               jqXhr = $.ajax( url ).done( function ( data ) {
-                       jqXhr = $spinner = undefined;
+               promise.done( function ( $contents ) {
+                       $spinner = undefined;
 
                        // Replace table contents
-                       $multipageimage.empty().append( $( data ).find( 'table.multipageimage' ).contents() );
+                       $multipageimage.empty().append( $contents.clone() );
 
                        bindPageNavigation( $multipageimage );
 
                                .extend( { title: mw.config.get( 'wgPageName' ), page: page } )
                                .toString();
 
-                       loadPage( uri );
+                       switchPage( uri );
                        e.preventDefault();
                } );
 
                $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
-                       loadPage( this.action + '?' + $( this ).serialize() );
+                       switchPage( this.action + '?' + $( this ).serialize() );
                        e.preventDefault();
                } );
        }
                        $( window ).on( 'popstate', function ( e ) {
                                var state = e.originalEvent.state;
                                if ( state && state.tag === 'mw-pagination' ) {
-                                       loadPage( location.href, true );
+                                       switchPage( location.href, true );
                                }
                        } );
                }