unpoly-rails 0.37.0 → 0.50.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -25
  3. data/LICENSE +1 -1
  4. data/README_RAILS.md +4 -2
  5. data/Rakefile +6 -1
  6. data/dist/unpoly.js +3192 -2198
  7. data/dist/unpoly.min.js +4 -3
  8. data/lib/assets/javascripts/unpoly/browser.coffee +51 -63
  9. data/lib/assets/javascripts/unpoly/bus.coffee +58 -33
  10. data/lib/assets/javascripts/unpoly/classes/cache.coffee +117 -0
  11. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_cascade.coffee +3 -3
  12. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_plan.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +57 -0
  14. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +52 -0
  15. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +95 -0
  16. data/lib/assets/javascripts/unpoly/classes/record.coffee +16 -0
  17. data/lib/assets/javascripts/unpoly/classes/request.coffee +228 -0
  18. data/lib/assets/javascripts/unpoly/classes/response.coffee +138 -0
  19. data/lib/assets/javascripts/unpoly/dom.coffee +151 -142
  20. data/lib/assets/javascripts/unpoly/feedback.coffee +67 -38
  21. data/lib/assets/javascripts/unpoly/form.coffee +156 -139
  22. data/lib/assets/javascripts/unpoly/history.coffee +22 -19
  23. data/lib/assets/javascripts/unpoly/layout.coffee +108 -90
  24. data/lib/assets/javascripts/unpoly/link.coffee +159 -158
  25. data/lib/assets/javascripts/unpoly/log.coffee +5 -5
  26. data/lib/assets/javascripts/unpoly/modal.coffee +93 -81
  27. data/lib/assets/javascripts/unpoly/motion.coffee +291 -250
  28. data/lib/assets/javascripts/unpoly/popup.coffee +67 -53
  29. data/lib/assets/javascripts/unpoly/protocol.coffee +67 -16
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +282 -211
  31. data/lib/assets/javascripts/unpoly/rails.coffee +3 -14
  32. data/lib/assets/javascripts/unpoly/syntax.coffee +54 -49
  33. data/lib/assets/javascripts/unpoly/tooltip.coffee +18 -25
  34. data/lib/assets/javascripts/unpoly/util.coffee +236 -477
  35. data/lib/assets/javascripts/unpoly.coffee +1 -1
  36. data/lib/unpoly/rails/inspector.rb +67 -22
  37. data/lib/unpoly/rails/version.rb +1 -1
  38. data/package.json +1 -1
  39. data/spec_app/Gemfile.lock +13 -13
  40. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  41. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -1
  42. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +10 -0
  43. data/spec_app/app/controllers/binding_test_controller.rb +19 -2
  44. data/spec_app/app/controllers/method_test_controller.rb +16 -0
  45. data/spec_app/app/views/layouts/jasmine_rails/spec_runner.html.erb +20 -0
  46. data/spec_app/app/views/method_test/form_target.erb +17 -0
  47. data/spec_app/app/views/method_test/page1.erb +11 -0
  48. data/spec_app/app/views/method_test/page2.erb +6 -0
  49. data/spec_app/app/views/pages/start.erb +33 -19
  50. data/spec_app/config/initializers/assets.rb +5 -0
  51. data/spec_app/config/routes.rb +3 -0
  52. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -27
  53. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +17 -0
  54. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +102 -0
  55. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -1
  56. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +5 -2
  57. data/spec_app/spec/javascripts/helpers/promise_state.js +18 -0
  58. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +9 -0
  59. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +22 -0
  60. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +11 -3
  61. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +10 -0
  62. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +5 -0
  63. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +13 -0
  64. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +13 -6
  65. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +92 -33
  66. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +64 -15
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
  68. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +1 -0
  69. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +759 -551
  70. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +155 -82
  71. data/spec_app/spec/javascripts/up/form_spec.js.coffee +490 -349
  72. data/spec_app/spec/javascripts/up/history_spec.js.coffee +226 -179
  73. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +253 -185
  74. data/spec_app/spec/javascripts/up/link_spec.js.coffee +416 -270
  75. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +459 -330
  76. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +198 -153
  77. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +9 -0
  78. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +240 -175
  79. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +38 -0
  80. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +777 -303
  81. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +24 -8
  82. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +40 -23
  83. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +80 -66
  84. data/spec_app/spec/javascripts/up/util_spec.js.coffee +227 -201
  85. data/spec_app/vendor/asset-libs/es6-promise-4.1.6/es6-promise.auto.js +1159 -0
  86. metadata +30 -7
  87. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +0 -7
  88. data/spec_app/spec/javascripts/helpers/to_equal_url.coffee +0 -11
@@ -17,12 +17,12 @@ The cache holds up to 70 responses for 5 minutes. You can configure the cache si
17
17
  Also the entire cache is cleared with every non-`GET` request (like `POST` or `PUT`).
18
18
 
19
19
  If you need to make cache-aware requests from your [custom JavaScript](/up.syntax),
20
- use [`up.ajax()`](/up.ajax).
20
+ use [`up.request()`](/up.request).
21
21
 
22
22
  \#\#\# Preloading links
23
23
 
24
24
  Unpoly also lets you speed up reaction times by [preloading
25
- links](/up-preload) when the user hovers over the click area (or puts the mouse/finger
25
+ links](/a-up-preload) when the user hovers over the click area (or puts the mouse/finger
26
26
  down). This way the response will already be cached when
27
27
  the user releases the mouse/finger.
28
28
 
@@ -35,8 +35,8 @@ that appears during a long-running request.
35
35
 
36
36
  Other Unpoly modules contain even more tricks to outsmart network latency:
37
37
 
38
- - [Instantaneous feedback for links that are currently loading](/up-active)
39
- - [Follow links on `mousedown` instead of `click`](/up-instant)
38
+ - [Instantaneous feedback for links that are currently loading](/a.up-active)
39
+ - [Follow links on `mousedown` instead of `click`](/a-up-instant)
40
40
 
41
41
  @class up.proxy
42
42
  ###
@@ -50,23 +50,23 @@ up.proxy = (($) ->
50
50
  pendingCount = undefined
51
51
  slowEventEmitted = undefined
52
52
 
53
- queuedRequests = []
53
+ queuedLoaders = []
54
54
 
55
55
  ###*
56
56
  @property up.proxy.config
57
- @param {Number} [config.preloadDelay=75]
58
- The number of milliseconds to wait before [`[up-preload]`](/up-preload)
57
+ @param {number} [config.preloadDelay=75]
58
+ The number of milliseconds to wait before [`[up-preload]`](/a-up-preload)
59
59
  starts preloading.
60
- @param {Number} [config.cacheSize=70]
60
+ @param {number} [config.cacheSize=70]
61
61
  The maximum number of responses to cache.
62
62
  If the size is exceeded, the oldest items will be dropped from the cache.
63
- @param {Number} [config.cacheExpiry=300000]
63
+ @param {number} [config.cacheExpiry=300000]
64
64
  The number of milliseconds until a cache entry expires.
65
65
  Defaults to 5 minutes.
66
- @param {Number} [config.slowDelay=300]
66
+ @param {number} [config.slowDelay=300]
67
67
  How long the proxy waits until emitting the [`up:proxy:slow` event](/up:proxy:slow).
68
68
  Use this to prevent flickering of spinners.
69
- @param {Number} [config.maxRequests=4]
69
+ @param {number} [config.maxRequests=4]
70
70
  The maximum number of concurrent requests to allow before additional
71
71
  requests are queued. This currently ignores preloading requests.
72
72
 
@@ -75,14 +75,14 @@ up.proxy = (($) ->
75
75
 
76
76
  Note that your browser might [impose its own request limit](http://www.browserscope.org/?category=network)
77
77
  regardless of what you configure here.
78
- @param {Array<String>} [config.wrapMethods]
78
+ @param {Array<string>} [config.wrapMethods]
79
79
  An array of uppercase HTTP method names. AJAX requests with one of these methods
80
80
  will be converted into a `POST` request and carry their original method as a `_method`
81
81
  parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347).
82
- @param {Array<String>} [config.safeMethods]
83
- An array of uppercase HTTP method names that are considered idempotent.
84
- The proxy cache will only cache idempotent requests and will clear the entire
85
- cache after a non-idempotent request.
82
+ @param {Array<string>} [config.safeMethods]
83
+ An array of uppercase HTTP method names that are considered [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1).
84
+ The proxy cache will only cache safe requests and will clear the entire
85
+ cache after an unsafe request.
86
86
  @stable
87
87
  ###
88
88
  config = u.config
@@ -94,19 +94,12 @@ up.proxy = (($) ->
94
94
  wrapMethods: ['PATCH', 'PUT', 'DELETE']
95
95
  safeMethods: ['GET', 'OPTIONS', 'HEAD']
96
96
 
97
- cacheKey = (request) ->
98
- normalizeRequest(request)
99
- [ request.url,
100
- request.method,
101
- u.requestDataAsQuery(request.data),
102
- request.target
103
- ].join('|')
104
-
105
- cache = u.cache
97
+ cache = new up.Cache
106
98
  size: -> config.cacheSize
107
99
  expiry: -> config.cacheExpiry
108
- key: cacheKey
109
- # log: 'up.proxy'
100
+ key: (request) -> up.Request.wrap(request).cacheKey()
101
+ cachable: (request) -> up.Request.wrap(request).isCachable()
102
+ # logPrefix: 'up.proxy'
110
103
 
111
104
  ###*
112
105
  Returns a cached response for the given request.
@@ -114,24 +107,30 @@ up.proxy = (($) ->
114
107
  Returns `undefined` if the given request is not currently cached.
115
108
 
116
109
  @function up.proxy.get
117
- @return {Promise}
118
- A promise for the response that is API-compatible with the
119
- promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
110
+ @return {Promise<up.Response>}
111
+ A promise for the response.
120
112
  @experimental
121
113
  ###
122
114
  get = (request) ->
123
- request = normalizeRequest(request)
124
- if isCachable(request)
125
- candidates = [request]
126
- unless request.target is 'html'
127
- requestForHtml = u.merge(request, target: 'html')
128
- candidates.push(requestForHtml)
129
- unless request.target is 'body'
130
- requestForBody = u.merge(request, target: 'body')
131
- candidates.push(requestForBody)
132
- for candidate in candidates
133
- if response = cache.get(candidate)
134
- return response
115
+ request = up.Request.wrap(request)
116
+ candidates = [request]
117
+
118
+ if request.target != 'html'
119
+ # Since <html> is the root tag, a request for the `html` selector
120
+ # will contain all other selectors.
121
+ requestForHtml = request.copy(target: 'html')
122
+ candidates.push(requestForHtml)
123
+
124
+ # Although <body> is not the root tag, we consider it the selector developers
125
+ # will use when they want to replace the entire page. Hence we consider it
126
+ # a suitable match for all other selectors, including `html`.
127
+ if request.target != 'body'
128
+ requestForBody = request.copy(target: 'body')
129
+ candidates.push(requestForBody)
130
+
131
+ for candidate in candidates
132
+ if response = cache.get(candidate)
133
+ return response
135
134
 
136
135
  cancelPreloadDelay = ->
137
136
  clearTimeout(preloadDelayTimer)
@@ -149,108 +148,100 @@ up.proxy = (($) ->
149
148
  config.reset()
150
149
  cache.clear()
151
150
  slowEventEmitted = false
152
- queuedRequests = []
151
+ queuedLoaders = []
153
152
 
154
153
  reset()
155
154
 
156
- normalizeRequest = (request) ->
157
- unless request._normalized
158
- request.method = u.normalizeMethod(request.method)
159
- request.url = u.normalizeUrl(request.url) if request.url
160
- request.target ||= 'body'
161
- request._normalized = true
162
- request
163
-
164
155
  ###*
165
- Makes a request to the given URL and caches the response.
166
- If the response was already cached, returns the HTML instantly.
167
-
168
- If requesting a URL that is not read-only, the response will
169
- not be cached and the entire cache will be cleared.
170
- Only requests with a method of `GET`, `OPTIONS` and `HEAD`
171
- are considered to be read-only.
156
+ Makes an AJAX request to the given URL.
172
157
 
173
158
  \#\#\# Example
174
159
 
175
- up.ajax('/search', data: { query: 'sunshine' }).then(function(data, status, xhr) {
176
- console.log('The response body is %o', data);
177
- }).fail(function(xhr, status, error) {
160
+ up.request('/search', data: { query: 'sunshine' }).then(function(response) {
161
+ console.log('The response text is %o', response.text);
162
+ }).fail(function() {
178
163
  console.error('The request failed');
179
164
  });
180
165
 
166
+ \#\#\# Caching
167
+
168
+ All responses are cached by default. If requesting a URL with a non-`GET` method, the response will
169
+ not be cached and the entire cache will be cleared.
170
+
171
+ You can configure caching with the [`up.proxy.config`](/up.proxy.config) property.
172
+
181
173
  \#\#\# Events
182
174
 
183
175
  If a network connection is attempted, the proxy will emit
184
176
  a [`up:proxy:load`](/up:proxy:load) event with the `request` as its argument.
185
- Once the response is received, a [`up:proxy:receive`](/up:proxy:receive) event will
177
+ Once the response is received, a [`up:proxy:loaded`](/up:proxy:loaded) event will
186
178
  be emitted.
187
179
 
188
- @function up.ajax
189
- @param {String} url
190
- @param {String} [request.method='GET']
191
- @param {String} [request.target='body']
192
- @param {Boolean} [request.cache]
193
- Whether to use a cached response, if available.
194
- If set to `false` a network connection will always be attempted.
195
- @param {Object} [request.headers={}]
196
- An object of additional header key/value pairs to send along
197
- with the request.
198
- @param {Object} [request.data={}]
199
- An object of request parameters.
200
- @param {String} [request.url]
180
+ @function up.request
181
+ @param {string} [url]
182
+ The URL for the request.
183
+
184
+ Instead of passing the URL as a string argument, you can also pass it as an `{ url }` option.
185
+ @param {string} [options.url]
201
186
  You can omit the first string argument and pass the URL as
202
187
  a `request` property instead.
203
- @param {String} [request.timeout]
204
- A timeout in milliseconds for the request.
188
+ @param {string} [options.method='GET']
189
+ The HTTP method for the options.
190
+ @param {boolean} [options.cache]
191
+ Whether to use a cached response for [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1)
192
+ requests, if available. If set to `false` a network connection will always be attempted.
193
+ @param {Object} [options.headers={}]
194
+ An object of additional HTTP headers.
195
+ @param {Object|Array|FormData} [options.data={}]
196
+ Parameters that should be sent as the request's payload.
197
+
198
+ Parameters may be passed as one of the following forms:
199
+
200
+ 1. An object where keys are param names and the values are param values
201
+ 2. An array of `{ name: 'param-name', value: 'param-value' }` objects
202
+ 3. A [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object
203
+ @param {string} [options.timeout]
204
+ A timeout in milliseconds.
205
205
 
206
206
  If [`up.proxy.config.maxRequests`](/up.proxy.config#config.maxRequests) is set, the timeout
207
207
  will not include the time spent waiting in the queue.
208
- @return
209
- A promise for the response that is API-compatible with the
210
- promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
208
+ @param {string} [options.target='body']
209
+ The CSS selector that will be sent as an [`X-Up-Target` header](/up.protocol#optimizing-responses).
210
+ @param {string} [options.failTarget='body']
211
+ The CSS selector that will be sent as an [`X-Up-Fail-Target` header](/up.protocol#optimizing-responses).
212
+ @return {Promise<up.Response>}
213
+ A promise for the response.
211
214
  @stable
212
215
  ###
213
- ajax = (args...) ->
214
-
216
+ makeRequest = (args...) ->
215
217
  options = u.extractOptions(args)
216
218
  options.url = args[0] if u.isGiven(args[0])
217
219
 
218
- forceCache = (options.cache == true)
219
220
  ignoreCache = (options.cache == false)
220
221
 
221
- request = u.only options,
222
- 'url',
223
- 'method',
224
- 'data',
225
- 'target',
226
- 'headers',
227
- 'timeout',
228
- '_normalized'
229
-
230
- request = normalizeRequest(request)
231
-
232
- pending = true
222
+ request = up.Request.wrap(options)
233
223
 
234
224
  # Non-GET requests always touch the network
235
225
  # unless `options.cache` is explicitly set to `true`.
236
226
  # These requests are never cached.
237
- if !isIdempotent(request) && !forceCache
227
+ if !request.isSafe()
228
+ # We clear the entire cache before an unsafe request, since we
229
+ # assume the user is writing a change.
238
230
  clear()
239
- promise = loadOrQueue(request)
231
+
240
232
  # If we have an existing promise matching this new request,
241
233
  # we use it unless `options.cache` is explicitly set to `false`.
242
- # The promise might still be pending.
243
- else if (promise = get(request)) && !ignoreCache
234
+ if !ignoreCache && (promise = get(request))
244
235
  up.puts 'Re-using cached response for %s %s', request.method, request.url
245
- pending = (promise.state() == 'pending')
246
- # If no existing promise is available, we make a network request.
247
236
  else
237
+ # If no existing promise is available, we make a network request.
248
238
  promise = loadOrQueue(request)
249
239
  set(request, promise)
250
- # Don't cache failed requests
251
- promise.fail -> remove(request)
240
+ # Uncache failed requests
241
+ promise.catch (e) ->
242
+ remove(request)
252
243
 
253
- if pending && !options.preload
244
+ if !options.preload
254
245
  # This might actually make `pendingCount` higher than the actual
255
246
  # number of outstanding requests. However, we need to cover the
256
247
  # following case:
@@ -262,27 +253,69 @@ up.proxy = (($) ->
262
253
  # - The request finishes.
263
254
  # This triggers `up:proxy:recover`.
264
255
  loadStarted()
265
- promise.always(loadEnded)
256
+ u.always promise, loadEnded
266
257
 
267
258
  promise
268
259
 
269
260
  ###*
270
- Returns whether the proxy is capable of caching the given request.
271
- Even if this returns `true`, only idempodent requests will be
272
- cached by default.
261
+ Makes an AJAX request to the given URL and caches the response.
273
262
 
274
- @function up.proxy.isCachable
275
- @internal
263
+ The function returns a promise that fulfills with the response text.
264
+
265
+ \#\#\# Example
266
+
267
+ up.request('/search', data: { query: 'sunshine' }).then(function(text) {
268
+ console.log('The response text is %o', text);
269
+ }).fail(function() {
270
+ console.error('The request failed');
271
+ });
272
+
273
+ @function up.ajax
274
+ @param {string} [url]
275
+ The URL for the request.
276
+
277
+ Instead of passing the URL as a string argument, you can also pass it as an `{ url }` option.
278
+ @param {string} [request.url]
279
+ You can omit the first string argument and pass the URL as
280
+ a `request` property instead.
281
+ @param {string} [request.method='GET']
282
+ The HTTP method for the request.
283
+ @param {boolean} [request.cache]
284
+ Whether to use a cached response for [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1)
285
+ requests, if available. If set to `false` a network connection will always be attempted.
286
+ @param {Object} [request.headers={}]
287
+ An object of additional header key/value pairs to send along
288
+ with the request.
289
+ @param {Object|Array|FormData} [options.data]
290
+ Parameters that should be sent as the request's payload.
291
+
292
+ Parameters may be passed as one of the following forms:
293
+
294
+ 1. An object where keys are param names and the values are param values
295
+ 2. An array of `{ name: 'param-name', value: 'param-value' }` objects
296
+ 3. A [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object
297
+ @param {string} [request.timeout]
298
+ A timeout in milliseconds for the request.
299
+
300
+ If [`up.proxy.config.maxRequests`](/up.proxy.config#config.maxRequests) is set, the timeout
301
+ will not include the time spent waiting in the queue.
302
+ @return {Promise<string>}
303
+ A promise for the response text.
304
+ @deprecated
305
+ Use [`up.request()`](/up.request) instead.
276
306
  ###
277
- isCachable = (request) ->
278
- not u.isFormData(request.data)
307
+ ajax = (args...) ->
308
+ up.log.warn('up.ajax() has been deprecated. Use up.request() instead.')
309
+ new Promise (resolve, reject) ->
310
+ pickResponseText = (response) -> resolve(response.text)
311
+ makeRequest(args...).then(pickResponseText, reject)
279
312
 
280
313
  ###*
281
314
  Returns `true` if the proxy is not currently waiting
282
315
  for a request to finish. Returns `false` otherwise.
283
316
 
284
317
  @function up.proxy.isIdle
285
- @return {Boolean}
318
+ @return {boolean}
286
319
  Whether the proxy is idle
287
320
  @experimental
288
321
  ###
@@ -294,7 +327,7 @@ up.proxy = (($) ->
294
327
  for a request to finish. Returns `false` otherwise.
295
328
 
296
329
  @function up.proxy.isBusy
297
- @return {Boolean}
330
+ @return {boolean}
298
331
  Whether the proxy is busy
299
332
  @experimental
300
333
  ###
@@ -302,19 +335,19 @@ up.proxy = (($) ->
302
335
  pendingCount > 0
303
336
 
304
337
  loadStarted = ->
305
- wasIdle = isIdle()
306
338
  pendingCount += 1
307
- if wasIdle
339
+ unless slowDelayTimer
308
340
  # Since the emission of up:proxy:slow might be delayed by config.slowDelay,
309
341
  # we wrap the mission in a function for scheduling below.
310
342
  emission = ->
311
343
  if isBusy() # a fast response might have beaten the delay
312
- up.emit('up:proxy:slow', message: 'Proxy is busy')
344
+ up.emit('up:proxy:slow', message: 'Proxy is slow to respond')
313
345
  slowEventEmitted = true
314
346
  slowDelayTimer = u.setTimer(config.slowDelay, emission)
315
347
 
348
+
316
349
  ###*
317
- This event is [emitted](/up.emit) when [AJAX requests](/up.ajax)
350
+ This event is [emitted](/up.emit) when [AJAX requests](/up.request)
318
351
  are taking long to finish.
319
352
 
320
353
  By default Unpoly will wait 300 ms for an AJAX request to finish
@@ -367,12 +400,15 @@ up.proxy = (($) ->
367
400
 
368
401
  loadEnded = ->
369
402
  pendingCount -= 1
370
- if isIdle() && slowEventEmitted
371
- up.emit('up:proxy:recover', message: 'Proxy is idle')
372
- slowEventEmitted = false
403
+
404
+ if isIdle()
405
+ cancelSlowDelay()
406
+ if slowEventEmitted
407
+ up.emit('up:proxy:recover', message: 'Proxy has recovered from slow response')
408
+ slowEventEmitted = false
373
409
 
374
410
  ###*
375
- This event is [emitted](/up.emit) when [AJAX requests](/up.ajax)
411
+ This event is [emitted](/up.emit) when [AJAX requests](/up.request)
376
412
  have [taken long to finish](/up:proxy:slow), but have finished now.
377
413
 
378
414
  See [`up:proxy:slow`](/up:proxy:slow) for more documentation on
@@ -391,49 +427,88 @@ up.proxy = (($) ->
391
427
 
392
428
  queue = (request) ->
393
429
  up.puts('Queuing request for %s %s', request.method, request.url)
394
- deferred = $.Deferred()
395
- entry =
396
- deferred: deferred
397
- request: request
398
- queuedRequests.push(entry)
399
- deferred.promise()
430
+ loader = -> load(request)
431
+ loader = u.previewable(loader)
432
+ queuedLoaders.push(loader)
433
+ loader.promise
400
434
 
401
435
  load = (request) ->
402
- up.emit('up:proxy:load', u.merge(request, message: ['Loading %s %s', request.method, request.url]))
436
+ eventProps =
437
+ request: request
438
+ message: ['Loading %s %s', request.method, request.url]
403
439
 
404
- # We will modify the request below for features like method wrapping.
405
- # Let's not change the original request which would confuse API clients
406
- # and cache key logic.
407
- request = u.copy(request)
440
+ if up.bus.nobodyPrevents('up:proxy:load', eventProps)
441
+ responsePromise = request.send()
442
+ u.always responsePromise, responseReceived
443
+ u.always responsePromise, pokeQueue
444
+ responsePromise
445
+ else
446
+ u.microtask(pokeQueue)
447
+ Promise.reject(new Error('Event up:proxy:load was prevented'))
408
448
 
409
- request.headers ||= {}
410
- request.headers[up.protocol.config.targetHeader] = request.target
449
+ ###*
450
+ This event is [emitted](/up.emit) before an [AJAX request](/up.request)
451
+ is sent over the network.
411
452
 
412
- if u.contains(config.wrapMethods, request.method)
413
- request.data = u.appendRequestData(request.data, up.protocol.config.methodParam, request.method)
414
- request.method = 'POST'
453
+ @event up:proxy:load
454
+ @param {up.Request} event.request
455
+ @param event.preventDefault()
456
+ Event listeners may call this method to prevent the request from being sent.
457
+ @experimental
458
+ ###
415
459
 
416
- if u.isFormData(request.data)
417
- # Disable jQuery's request data processing so we can pass
418
- # a FormData object (http://stackoverflow.com/a/5976031)
419
- request.contentType = false
420
- request.processData = false
460
+ registerAliasForRedirect = (response) ->
461
+ request = response.request
462
+ if request.url != response.url
463
+ newRequest = request.copy(
464
+ method: response.method
465
+ url: response.url
466
+ )
467
+ up.proxy.alias(request, newRequest)
468
+
469
+ responseReceived = (response) ->
470
+ if response.isFatalError()
471
+ up.emit 'up:proxy:fatal',
472
+ message: 'Fatal error during request'
473
+ request: response.request
474
+ response: response
475
+ else
476
+ registerAliasForRedirect(response) unless response.isError()
477
+ up.emit 'up:proxy:loaded',
478
+ message: ['Server responded with HTTP %d (%d bytes)', response.status, response.text.length]
479
+ request: response.request
480
+ response: response
421
481
 
422
- promise = $.ajax(request)
423
- promise.done (data, textStatus, xhr) -> responseReceived(request, xhr)
424
- promise.fail (xhr, textStatus, errorThrown) -> responseReceived(request, xhr)
425
- promise
482
+ ###*
483
+ This event is [emitted](/up.emit) when the response to an
484
+ [AJAX request](/up.request) has been received.
485
+
486
+ Note that this event will also be emitted when the server signals an
487
+ error with an HTTP status like `500`. Only if the request
488
+ encounters a fatal error (like a loss of network connectivity),
489
+ [`up:proxy:fatal`](/up:proxy:fatal) is emitted instead.
490
+
491
+ @event up:proxy:loaded
492
+ @param {up.Request} event.request
493
+ @param {up.Response} event.response
494
+ @experimental
495
+ ###
426
496
 
427
- responseReceived = (request, xhr) ->
428
- up.emit('up:proxy:received', u.merge(request, message: ['Server responded with %s %s (%d bytes)', xhr.status, xhr.statusText, xhr.responseText?.length]))
429
- pokeQueue()
497
+ ###*
498
+ This event is [emitted](/up.emit) when an [AJAX request](/up.request)
499
+ encounters fatal error like a timeout or loss of network connectivity.
500
+
501
+ Note that this event will *not* be emitted when the server produces an
502
+ error message with an HTTP status like `500`. When the server can produce
503
+ any response, [`up:proxy:loaded`](/up:proxy:loaded) is emitted instead.
504
+
505
+ @event up:proxy:fatal
506
+ ###
430
507
 
431
508
  pokeQueue = ->
432
- if entry = queuedRequests.shift()
433
- promise = load(entry.request)
434
- promise.done (args...) -> entry.deferred.resolve(args...)
435
- promise.fail (args...) -> entry.deferred.reject(args...)
436
- return
509
+ queuedLoaders.shift()?()
510
+ # Don't return the promise from the loader above
511
+ return undefined
437
512
 
438
513
  ###*
439
514
  Makes the proxy assume that `newRequest` has the same response as the
@@ -453,12 +528,11 @@ up.proxy = (($) ->
453
528
  Manually stores a promise for the response to the given request.
454
529
 
455
530
  @function up.proxy.set
456
- @param {String} request.url
457
- @param {String} [request.method='GET']
458
- @param {String} [request.target='body']
459
- @param {Promise} response
460
- A promise for the response that is API-compatible with the
461
- promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
531
+ @param {string} request.url
532
+ @param {string} [request.method='GET']
533
+ @param {string} [request.target='body']
534
+ @param {Promise<up.Response>} response
535
+ A promise for the response.
462
536
  @experimental
463
537
  ###
464
538
  set = cache.set
@@ -470,9 +544,9 @@ up.proxy = (($) ->
470
544
  automatically removes cache entries.
471
545
 
472
546
  @function up.proxy.remove
473
- @param {String} request.url
474
- @param {String} [request.method='GET']
475
- @param {String} [request.target='body']
547
+ @param {string} request.url
548
+ @param {string} [request.method='GET']
549
+ @param {string} [request.target='body']
476
550
  @experimental
477
551
  ###
478
552
  remove = cache.remove
@@ -481,41 +555,18 @@ up.proxy = (($) ->
481
555
  Removes all cache entries.
482
556
 
483
557
  Unpoly also automatically clears the cache whenever it processes
484
- a request with a non-GET HTTP method.
558
+ a request with an [unsafe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1)
559
+ HTTP method like `POST`.
485
560
 
486
561
  @function up.proxy.clear
487
562
  @stable
488
563
  ###
489
564
  clear = cache.clear
490
565
 
491
- ###*
492
- This event is [emitted](/up.emit) before an [AJAX request](/up.ajax)
493
- is starting to load.
494
-
495
- @event up:proxy:load
496
- @param event.url
497
- @param event.method
498
- @param event.target
499
- @experimental
500
- ###
501
-
502
- ###*
503
- This event is [emitted](/up.emit) when the response to an [AJAX request](/up.ajax)
504
- has been received.
505
-
506
- @event up:proxy:received
507
- @param event.url
508
- @param event.method
509
- @param event.target
510
- @experimental
511
- ###
512
-
513
- isIdempotent = (request) ->
514
- normalizeRequest(request)
515
- u.contains(config.safeMethods, request.method)
566
+ up.bus.renamedEvent('up:proxy:received', 'up:proxy:loaded')
516
567
 
517
568
  checkPreload = ($link) ->
518
- delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay
569
+ delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay
519
570
  unless $link.is($waitingLink)
520
571
  $waitingLink = $link
521
572
  cancelPreloadDelay()
@@ -528,60 +579,80 @@ up.proxy = (($) ->
528
579
  preloadDelayTimer = setTimeout(block, delay)
529
580
 
530
581
  ###*
582
+ Preloads the given link.
583
+
584
+ When the link is clicked later, the response will already be cached,
585
+ making the interaction feel instant.
586
+
531
587
  @function up.proxy.preload
532
- @param {String|Element|jQuery}
588
+ @param {string|Element|jQuery}
533
589
  The element whose destination should be preloaded.
534
590
  @return
535
- A promise that will be resolved when the request was loaded and cached
591
+ A promise that will be fulfilled when the request was loaded and cached
536
592
  @experimental
537
593
  ###
538
- preload = (linkOrSelector, options) ->
594
+ preload = (linkOrSelector) ->
539
595
  $link = $(linkOrSelector)
540
- options = u.options(options)
541
596
 
542
- method = up.link.followMethod($link, options)
543
- if isIdempotent(method: method)
544
- up.log.group "Preloading link %o", $link, ->
545
- options.preload = true
546
- up.follow($link, options)
597
+ if up.link.isSafe($link)
598
+ up.log.group "Preloading link %o", $link.get(0), ->
599
+ variant = up.link.followVariantForLink($link)
600
+ variant.preloadLink($link)
547
601
  else
548
- up.puts("Won't preload %o due to unsafe method %s", $link, method)
549
- u.resolvedPromise()
602
+ Promise.reject(new Error("Won't preload unsafe link"))
603
+
604
+ ###*
605
+ @internal
606
+ ###
607
+ isSafeMethod = (method) ->
608
+ u.contains(config.safeMethods, method)
609
+
610
+ ###*
611
+ @internal
612
+ ###
613
+ wrapMethod = (method, data, appendOpts) ->
614
+ if u.contains(config.wrapMethods, method)
615
+ data = u.appendRequestData(data, up.protocol.config.methodParam, method, appendOpts)
616
+ method = 'POST'
617
+ [method, data]
550
618
 
551
619
  ###*
552
620
  Links with an `up-preload` attribute will silently fetch their target
553
621
  when the user hovers over the click area, or when the user puts her
554
- mouse/finger down (before releasing). This way the
555
- response will already be cached when the user performs the click,
622
+ mouse/finger down (before releasing).
623
+
624
+ When the link is clicked later, the response will already be cached,
556
625
  making the interaction feel instant.
557
626
 
558
- @selector [up-preload]
627
+ @selector a[up-preload]
559
628
  @param [up-delay=75]
560
629
  The number of milliseconds to wait between hovering
561
630
  and preloading. Increasing this will lower the load in your server,
562
631
  but will also make the interaction feel less instant.
563
632
  @stable
564
633
  ###
565
- up.on 'mouseover mousedown touchstart', '[up-preload]', (event, $element) ->
566
- # Don't do anything if we are hovering over the child
567
- # of a link. The actual link will receive the event
568
- # and bubble in a second.
569
- unless up.link.childClicked(event, $element)
634
+ up.on 'mouseover mousedown touchstart', 'a[up-preload], [up-href][up-preload]', (event, $element) ->
635
+ # Don't do anything if we are hovering over the child of a link.
636
+ # The actual link will receive the event and bubble in a second.
637
+ if !up.link.childClicked(event, $element) && up.link.isSafe($element)
570
638
  checkPreload($element)
571
639
 
572
640
  up.on 'up:framework:reset', reset
573
641
 
574
642
  preload: preload
575
643
  ajax: ajax
644
+ request: makeRequest
576
645
  get: get
577
646
  alias: alias
578
647
  clear: clear
579
648
  remove: remove
580
649
  isIdle: isIdle
581
650
  isBusy: isBusy
582
- isCachable: isCachable
651
+ isSafeMethod: isSafeMethod
652
+ wrapMethod: wrapMethod
583
653
  config: config
584
654
 
585
655
  )(jQuery)
586
656
 
587
657
  up.ajax = up.proxy.ajax
658
+ up.request = up.proxy.request