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
)()