make m.request exception follow promise resolution procedure

This commit is contained in:
Leo Horie 2014-05-20 22:53:30 -04:00
parent 715336d7c5
commit e332ffe473
6 changed files with 128 additions and 26 deletions

View file

@ -1,10 +1,24 @@
## Change Log
[v0.1.14](/mithril/archive/v0.1.14) - maintenance
### News:
- The signature of `m` now accepts virtual elements as the second parameter of the function.
### Bug Fixes:
- Resolving promises early without a value now works [#85](https://github.com/lhorie/mithril.js/issues/85)
- Throwing exceptions within `m.request` now follow the same resolution procedure as `m.deferred` [#86](https://github.com/lhorie/mithril.js/issues/85)
- Promises now always update their `m.prop` on success (and leave the m.prop alone on error)
---
[v0.1.13](/mithril/archive/v0.1.13) - maintenance
### News:
- m.module now runs clean-up code in root module controllers that implement an `onunload` instance method [82](https://github.com/lhorie/mithril.js/issues/82)
- m.module now runs clean-up code in root module controllers that implement an `onunload` instance method [#82](https://github.com/lhorie/mithril.js/issues/82)
### Bug Fixes:

View file

@ -10,7 +10,7 @@ Each computation function takes a value as a parameter and is expected to return
The deferred object returned by `m.deferred` has two methods: `resolve` and `reject`, and one property called `promise`. The methods can be called to dispatch a value to the promise tree. The `promise` property is the root of the promise tree. It has a method `then` which takes a `successCallback` and a `errorCallback` callbacks. Calling the `then` method attaches the computations represented by `successCallback` and `errorCallback` to the promise, which will be called when either `resolve` or `reject` is called. The `then` method returns a child promise, which, itself, can have more child promises, recursively.
The `promise` object is actually a function - specifically, it's an [`m.prop`](mithril.prop.md) getter-setter, which gets populated with the value returned by `successCallback` or `errorCallback` (depending on whether `resolve` or `reject` got called).
The `promise` object is actually a function - specifically, it's an [`m.prop`](mithril.prop.md) getter-setter, which gets populated with the value returned by `successCallback` if the promise is resolved successfully.
Note that Mithril promises are not automatically integrated to its automatic redrawing system. If you wish to use third party asynchronous libraries (for example, `jQuery.ajax`), you should also consider using [`m.startComputation` / `m.endComputation`](mithril.computation.md) if you want views to redraw after requests complete.
@ -33,6 +33,8 @@ greetAsync()
.then(function(value) {console.log(value)}); //logs "hello world" after 1 second
```
---
#### Retrieving a value via the getter-setter API
```javascript
@ -58,6 +60,8 @@ setTimeout(function() {
}, 2000)
```
---
#### Integrating to the Mithril redrawing system
```javascript
@ -77,6 +81,54 @@ var greetAsync = function() {
---
### Recoverable vs Unrecoverable errors
Recoverable errors are exceptions that are deliberately thrown by the application developer to signal an abnormal condition in the business logic. You can throw errors in this way if a computation needs to forward a failure condition to downstream promises.
In the example below we throw a recoverable error. It rejects the subsequent promise, and logs the string `"error"` to the console
```javascript
//standalone usage
var greetAsync = function() {
var deferred = m.deferred();
setTimeout(function() {
deferred.resolve("hello");
}, 1000);
return deferred.promise;
};
greetAsync()
.then(function(data) {
if (data != "hi") throw new Error("wrong greeting")
})
.then(function() {
console.log("success")
}, function() {
console.log("error")
})
```
Unrecoverable errors are exceptions that happen at runtime due to bugs in the code.
In the example below, calling the inexistent `foo.bar.baz` triggers a `ReferenceError`. Mithril does not handle this error in any way: it aborts execution and dumps the error information in the console.
```javascript
//standalone usage
var greetAsync = function() {
var deferred = m.deferred();
setTimeout(function() {
deferred.resolve("hello");
}, 1000);
return deferred.promise;
};
greetAsync().then(function() {
foo.bar.baz()
})
```
---
### Differences from Promises/A+
For the most part, Mithril promises behave as you'd expect a [Promise/A+](http://promises-aplus.github.io/promises-spec/) promise to behave, but with a few key differences:
@ -115,6 +167,8 @@ console.log(2)
In the example above, A+ promises are required to log `2` before logging `1`, whereas Mithril logs `1` before `2`. Typically `resolve`/`reject` are called asynchronously after the `then` method is called, so normally this difference does not matter.
One final difference is in how Mithril handles exceptions: if exceptions are thrown from within a success or error callbacks, A+ promises always reject the downstreams (and thus swallow the exception). Mithril does so only with errors that are subclasses of the Error class. Errors that are instances of the Error class itself are not caught by Mithril. This allows unrecoverable runtime errors to get thrown to the console w/ standard stack traces, while allowing developers to create application-space errors normally.
---
### Signature

View file

@ -224,7 +224,7 @@ VirtualElement m(String selector [, Attributes attributes] [, Children children]
where:
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<any | void config(DOMElement element, Boolean isInitialized)>
Children :: String text | Array<String text | VirtualElement virtualElement | SubtreeDirective directive | Children children>
Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array<Children children>
SubtreeDirective :: Object { String subtree }
```

View file

@ -94,7 +94,7 @@ app.view = function(ctrl) {
void render(DOMElement rootElement, Children children)
where:
Children :: String text | Array<String text | VirtualElement virtualElement | SubtreeDirective directive | Children children>
Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array<Children children>
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<Any | void config(DOMElement element)>
SubtreeDirective :: Object { String subtree }

View file

@ -5,7 +5,7 @@ Mithril = m = new function app(window) {
function m() {
var args = arguments
var hasAttrs = type.call(args[1]) == "[object Object]"
var hasAttrs = type.call(args[1]) == "[object Object]" && !("tag" in args[1]) && !("subtree" in args[1])
var attrs = hasAttrs ? args[1] : {}
var classAttrName = "class" in attrs ? "class" : "className"
var cell = selectorCache[args[0]]
@ -176,6 +176,7 @@ Mithril = m = new function app(window) {
}
function autoredraw(callback, object) {
return function(e) {
e = e || event
m.startComputation()
try {return callback.call(object, e)}
finally {m.endComputation()}
@ -255,7 +256,10 @@ Mithril = m = new function app(window) {
}
m.withAttr = function(prop, withAttrCallback) {
return function(e) {withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop))}
return function(e) {
e = e || event
withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop))
}
}
//routing
@ -323,6 +327,7 @@ Mithril = m = new function app(window) {
}
}
function routeUnobtrusive(e) {
e = e || event
if (e.ctrlKey || e.metaKey || e.which == 2) return
e.preventDefault()
m.route(e.currentTarget[m.route.mode].slice(modes[m.route.mode].length))
@ -345,10 +350,10 @@ Mithril = m = new function app(window) {
var none = {}
m.deferred = function() {
var resolvers = [], rejecters = [], resolved = none, rejected = none
var resolvers = [], rejecters = [], resolved = none, rejected = none, promise = m.prop()
var object = {
resolve: function(value) {
if (resolved === none) resolved = value
if (resolved === none) promise(resolved = value)
for (var i = 0; i < resolvers.length; i++) resolvers[i](value)
resolvers.length = rejecters.length = 0
},
@ -357,7 +362,7 @@ Mithril = m = new function app(window) {
for (var i = 0; i < rejecters.length; i++) rejecters[i](value)
resolvers.length = rejecters.length = 0
},
promise: m.prop()
promise: promise
}
object.promise.resolvers = resolvers
object.promise.then = function(success, error) {
@ -464,28 +469,25 @@ Mithril = m = new function app(window) {
xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data)
xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize)
xhrOptions.onload = xhrOptions.onerror = function(e) {
var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity
var response = unwrap(deserialize(extract(e.target, xhrOptions)))
if (response instanceof Array && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
try {
e = e || event
var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity
var response = unwrap(deserialize(extract(e.target, xhrOptions)))
if (response instanceof Array && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
}
else if (xhrOptions.type) response = new xhrOptions.type(response)
deferred[e.type == "load" ? "resolve" : "reject"](response)
}
catch (e) {
if (e instanceof Error && e.constructor !== Error) throw e
else deferred.reject(e)
}
else if (xhrOptions.type) response = new xhrOptions.type(response)
deferred.promise(response)
deferred[e.type == "load" ? "resolve" : "reject"](response)
if (xhrOptions.background !== true) m.endComputation()
}
ajax(xhrOptions)
deferred.promise.then = propBinder(deferred.promise)
return deferred.promise
}
function propBinder(promise) {
var bind = promise.then
return function(success, error) {
var next = bind(function(value) {return next(success(value))}, function(value) {return next(error(value))})
next.then = propBinder(next)
return next
}
}
//testing API
m.deps = function(mock) {return window = mock}

View file

@ -18,7 +18,7 @@ function testMithril(mock) {
test(function() {return m("div", {title: "bar"}, [m("div")]).children[0].tag === "div"})
test(function() {return m("div", ["a", "b"]).children.length === 2})
test(function() {return m("div", [m("div")]).children[0].tag === "div"})
test(function() {return m("div", m("div")).attrs.tag === "div"}) //yes, this is expected behavior: see method signature
test(function() {return m("div", m("div")).children.tag === "div"}) //yes, this is expected behavior: see method signature
test(function() {return m("div", [undefined]).tag === "div"})
test(function() {return m("div", [{foo: "bar"}])}) //as long as it doesn't throw errors, it's fine
test(function() {return m("svg", [m("g")])})
@ -619,6 +619,22 @@ function testMithril(mock) {
e.target.onload(e)
return prop().url === "http://domain.com:80/foo"
})
test(function() {
var error = m.prop("no error")
var prop = m.request({method: "GET", url: "test", deserialize: function() {throw new Error("error occurred")}}).then(null, error)
var e = mock.XMLHttpRequest.$events.pop()
e.target.onload(e)
return prop() === undefined && error().message === "error occurred"
})
test(function() {
var error = m.prop("no error"), exception
var prop = m.request({method: "GET", url: "test", deserialize: function() {throw new SyntaxError("error occurred")}}).then(null, error)
var event = mock.XMLHttpRequest.$events.pop()
try {event.target.onload(event)}
catch (e) {exception = e}
m.endComputation()
return prop() === undefined && error() === "no error" && exception.message == "error occurred"
})
//m.deferred
test(function() {
@ -760,6 +776,22 @@ function testMithril(mock) {
})
return value === 1
})
test(function() {
var deferred = m.deferred(), value
deferred.resolve(1)
return deferred.promise() === 1
})
test(function() {
var deferred = m.deferred(), value
var promise = deferred.promise.then(function(data) {return data + 1})
deferred.resolve(1)
return promise() === 2
})
test(function() {
var deferred = m.deferred(), value
deferred.reject(1)
return deferred.promise() === undefined
})
//m.sync
test(function() {