jquery.suggestions: Support caching results to save http requests
authorTimo Tijhof <krinklemail@gmail.com>
Wed, 21 May 2014 01:18:32 +0000 (03:18 +0200)
committerKrinkle <krinklemail@gmail.com>
Sat, 7 Jun 2014 08:13:36 +0000 (08:13 +0000)
When typing the same thing multiple times, or when pressing backspace
(thus reverting to an earlier known value), or otherwise ending
up with the same value, we can re-use the data we already used.

Doesn't happen in the basic case of typing one thing without mistakes
and selecting a results, but happens more than one might think.

People are often impatient, or even use the suggestions as their
search results and based on that might try something else and then
go back (e.g. try "Foo", find a good result but not ideal, try "Bar",
even worse, go back to "Foo", select "Foo thing").

During this, requests for "B", "Ba", "F", "Fo" and "Foo" would be
fired more than once (B-Ba when backspacing, F-Fo-Foo when typing it
the second time, or simply using ctrl-Z a couple times).

By default cache disabled (individual users of this plugin have
to opt-in). When enabled it defaults to an expiry of 1 minute.

Also:
* Cleaned up computing of $textbox.val(), it was being computed several
  times in a synchronous block.
* Simplified bool cast from foo ? true : false, to !!foo.

Bug: 65581
Change-Id: Ib10c65f6ab31773b7f517b2f9c3cc7c7b93c6d39

resources/src/jquery/jquery.suggestions.js

index 7a32076..08ed5d9 100644 (file)
  *             Type: Number, Range: 1 - 100, Default: 7
  * delay: Number of ms to wait for the user to stop typing
  *             Type: Number, Range: 0 - 1200, Default: 120
+ * cache: Whether to cache results from a fetch
+ *             Type: Boolean, Default: false
+ * cacheMaxAge: Number of ms to cache results from a fetch
+ *             Type: Number, Range: 1 - Infinity, Default: 60000 (1 minute)
  * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked
  *             Type: Boolean, Default: false
  * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set
@@ -47,6 +51,8 @@
  */
 ( function ( $ ) {
 
+var hasOwn = Object.hasOwnProperty;
+
 $.suggestions = {
        /**
         * Cancel any delayed maybeFetch() call and callback the context so
@@ -90,18 +96,44 @@ $.suggestions = {
         */
        update: function ( context, delayed ) {
                function maybeFetch() {
+                       var val = context.data.$textbox.val(),
+                               cache = context.data.cache,
+                               cacheHit;
+
                        // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
                        // if the textbox is empty then clear the result div, but leave other settings intouched
-                       if ( context.data.$textbox.val().length === 0 ) {
+                       if ( val.length === 0 ) {
                                $.suggestions.hide( context );
                                context.data.prevText = '';
                        } else if (
-                               context.data.$textbox.val() !== context.data.prevText ||
+                               val !== context.data.prevText ||
                                !context.data.$container.is( ':visible' )
                        ) {
-                               if ( typeof context.config.fetch === 'function' ) {
-                                       context.data.prevText = context.data.$textbox.val();
-                                       context.config.fetch.call( context.data.$textbox, context.data.$textbox.val() );
+                               context.data.prevText = val;
+                               // Try cache first
+                               if ( context.config.cache && hasOwn.call( cache, val ) ) {
+                                       if ( +new Date() - cache[ val ].timestamp < context.config.cacheMaxAge ) {
+                                               context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions );
+                                               cacheHit = true;
+                                       } else {
+                                               // Cache expired
+                                               delete cache[ val ];
+                                       }
+                               }
+                               if ( !cacheHit && typeof context.config.fetch === 'function' ) {
+                                       context.config.fetch.call(
+                                               context.data.$textbox,
+                                               val,
+                                               function ( suggestions ) {
+                                                       context.data.$textbox.suggestions( 'suggestions', suggestions );
+                                                       if ( context.config.cache ) {
+                                                               cache[ val ] = {
+                                                                       suggestions: suggestions,
+                                                                       timestamp: +new Date()
+                                                               };
+                                                       }
+                                               }
+                                       );
                                }
                        }
 
@@ -303,13 +335,17 @@ $.suggestions = {
                        case 'delay':
                                context.config[property] = Math.max( 0, Math.min( 1200, value ) );
                                break;
+                       case 'cacheMaxAge':
+                               context.config[property] = Math.max( 1, value );
+                               break;
                        case 'maxExpandFactor':
                                context.config[property] = Math.max( 1, value );
                                break;
+                       case 'cache':
                        case 'submitOnClick':
                        case 'positionFromLeft':
                        case 'highlightInput':
-                               context.config[property] = value ? true : false;
+                               context.config[property] = !!value;
                                break;
                }
        },
@@ -481,6 +517,8 @@ $.fn.suggestions = function () {
                                        suggestions: [],
                                        maxRows: 7,
                                        delay: 120,
+                                       cache: false,
+                                       cacheMaxAge: 60000,
                                        submitOnClick: false,
                                        maxExpandFactor: 3,
                                        expandFrom: 'auto',
@@ -519,6 +557,9 @@ $.fn.suggestions = function () {
                                // Text in textbox when suggestions were last fetched
                                prevText: null,
 
+                               // Cache of fetched suggestions
+                               cache: {},
+
                                // Number of results visible without scrolling
                                visibleResults: 0,