lib/assets/javascripts/up/proxy.js.coffee in upjs-rails-0.7.8 vs lib/assets/javascripts/up/proxy.js.coffee in upjs-rails-0.8.0

- old
+ new

@@ -2,30 +2,91 @@ Caching and preloading ====================== All HTTP requests go through the Up.js proxy. It caches a [limited](/up.proxy#up.proxy.defaults) number of server responses - for a [limited](/up.proxy#up.proxy.defaults) amount of time, +for a [limited](/up.proxy#up.proxy.defaults) amount of time, making requests to these URLs return insantly. The cache is cleared whenever the user makes a non-`GET` request (like `POST`, `PUT` or `DELETE`). The proxy can also used to speed up reaction times by preloading links when the user hovers over the click area (or puts the mouse/finger down before releasing). This way the -response will already be cached when the user performs the click. +response will already be cached when the user performs the click. +Spinners +--------- + +You can listen to [framework events](/up.bus) to implement a spinner +(progress indicator) that appears during a long-running request, +and disappears once the response has been received: + + <div class="spinner">Please wait!</div> + +Here is the Javascript to make it alive: + + up.compiler('.spinner', function($element) { + + show = function() { $element.show() }; + hide = function() { $element.hide() }; + + up.bus.on('proxy:busy', show); + up.bus.on('proxy:idle', hide); + + return function() { + up.bus.off('proxy:busy', show); + up.bus.off('proxy:idle', hide); + }; + + }); + +The `proxy:busy` event will be emitted after a delay of 300 ms +to prevent the spinner from flickering on and off. +You can change (or remove) this delay like this: + + up.proxy.defaults({ busyDelay: 150 }); + @class up.proxy ### up.proxy = (-> - config = + u = up.util + + cache = undefined + $waitingLink = undefined + preloadDelayTimer = undefined + busyDelayTimer = undefined + pendingCount = undefined + config = undefined + busyEventEmitted = undefined + FACTORY_CONFIG = + busyDelay: 300 preloadDelay: 75 cacheSize: 70 cacheExpiry: 1000 * 60 * 5 + cancelPreloadDelay = -> + clearTimeout(preloadDelayTimer) + preloadDelayTimer = null + + cancelBusyDelay = -> + clearTimeout(busyDelayTimer) + busyDelayTimer = null + + reset = -> + cache = {} + $waitingLink = null + cancelPreloadDelay() + cancelBusyDelay() + pendingCount = 0 + config = u.copy(FACTORY_CONFIG) + busyEventEmitted = false + + reset() + ###* @method up.proxy.defaults @param {Number} [options.preloadDelay=75] The number of milliseconds to wait before [`[up-preload]`](#up-preload) starts preloading. @@ -33,21 +94,17 @@ The maximum number of responses to cache. If the size is exceeded, the oldest items will be dropped from the cache. @param {Number} [options.cacheExpiry=300000] The number of milliseconds until a cache entry expires. Defaults to 5 minutes. + @param {Number} [options.busyDelay=300] + How long the proxy waits until emitting the `proxy:busy` [event](/up.bus). + Use this to prevent flickering of spinners. ### defaults = (options) -> u.extend(config, options) - - cache = {} - - u = up.util - - $waitingLink = null - delayTimer = null - + cacheKey = (request) -> normalizeRequest(request) [ request.url, request.method, request.data, @@ -80,19 +137,24 @@ alias = (oldRequest, newRequest) -> u.debug("Aliasing %o to %o", oldRequest, newRequest) if promise = get(oldRequest) set(newRequest, promise) - + ###* Makes a request to the given URL and caches the response. If the response was already cached, returns the HTML instantly. If requesting a URL that is not read-only, the response will not be cached and the entire cache will be cleared. Only requests with a method of `GET`, `OPTIONS` and `HEAD` are considered to be read-only. + + If a network connection is attempted, the proxy will emit + a `proxy:load` event with the `request` as its argument. + Once the response is received, a `proxy:receive` event will + be emitted. @method up.proxy.ajax @param {String} request.url @param {String} [request.method='GET'] @param {String} [request.selector] @@ -104,27 +166,95 @@ forceCache = u.castsToTrue(options.cache) ignoreCache = u.castsToFalse(options.cache) request = u.only(options, 'url', 'method', 'data', 'selector', '_normalized') + pending = true + # We don't cache non-GET responses unless `options.cache` # is explicitly set to `true`. if !isIdempotent(request) && !forceCache clear() - promise = u.ajax(request) + promise = load(request) # If a cached response is available, we use it unless # `options.cache` is explicitly set to `false`. else if (promise = get(request)) && !ignoreCache - promise + pending = (promise.state() == 'pending') else - promise = u.ajax(request) + promise = load(request) set(request, promise) + if pending && !options.preload + # This will actually make `pendingCount` higher than the actual + # number of outstanding requests. However, we need to cover the + # following case: + # + # - User starts preloading a request. + # This triggers *no* `proxy:busy`. + # - User starts loading the request (without preloading). + # This triggers `proxy:busy`. + # - The request finishes. + # This triggers `proxy:idle`. + loadStarted() + promise.then(loadEnded) + promise SAFE_HTTP_METHODS = ['GET', 'OPTIONS', 'HEAD'] - + + ###* + Returns `true` if the proxy is not currently waiting + for a request to finish. Returns `false` otherwise. + + The proxy will also emit an `proxy:idle` [event](/up.bus) if it + used to busy, but is now idle. + + @method up.proxy.idle + @return {Boolean} Whether the proxy is idle + ### + idle = -> + pendingCount == 0 + + ###* + Returns `true` if the proxy is currently waiting + for a request to finish. Returns `false` otherwise. + + The proxy will also emit an `proxy:busy` [event](/up.bus) if it + used to idle, but is now busy. + + @method up.proxy.busy + @return {Boolean} Whether the proxy is busy + ### + busy = -> + pendingCount > 0 + + loadStarted = -> + wasIdle = idle() + pendingCount += 1 + if wasIdle + emission = -> + if busy() # a fast response might have beaten the delay + up.bus.emit('proxy:busy') + busyEventEmitted = true + if config.busyDelay > 0 + busyDelayTimer = setTimeout(emission, config.busyDelay) + else + emission() + + loadEnded = -> + pendingCount -= 1 + if idle() && busyEventEmitted + up.bus.emit('proxy:idle') + busyEventEmitted = false + + load = (request) -> + up.bus.emit('proxy:load', request) + promise = u.ajax(request) + promise.then -> + up.bus.emit('proxy:receive', request) + promise + isIdempotent = (request) -> normalizeRequest(request) u.contains(SAFE_HTTP_METHODS, request.method) isFresh = (promise) -> @@ -178,20 +308,16 @@ checkPreload = ($link) -> delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay unless $link.is($waitingLink) $waitingLink = $link - cancelDelay() + cancelPreloadDelay() curriedPreload = -> preload($link) - startDelay(curriedPreload, delay) + startPreloadDelay(curriedPreload, delay) - startDelay = (block, delay) -> - delayTimer = setTimeout(block, delay) - - cancelDelay = -> - clearTimeout(delayTimer) - delayTimer = null + startPreloadDelay = (block, delay) -> + preloadDelayTimer = setTimeout(block, delay) ###* @protected @method up.proxy.preload @return @@ -207,14 +333,10 @@ options.preload = true up.link.follow($link, options) else u.debug("Won't preload %o due to unsafe method %o", $link, method) u.resolvedPromise() - - reset = -> - cancelDelay() - cache = {} up.bus.on 'framework:reset', reset ###* Links with an `up-preload` attribute will silently fetch their target @@ -242,8 +364,10 @@ get: get set: set alias: alias clear: clear remove: remove + idle: idle + busy: busy defaults: defaults )()