Skip to content

Commit

Permalink
enable es6 promise compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
Leo Horie committed Sep 4, 2014
1 parent d11087e commit 36999e6
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 31 deletions.
30 changes: 27 additions & 3 deletions docs/mithril.deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ var greetAsync = function() {

### 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 has one difference:
Mithril promises atempt to execute synchronously if possible.
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 they have a few differences.

To illustrate the difference between Mithril and A+ promises, consider the code below:
#### Synchronous execution

Mithril promises attempt to execute synchronously if possible. To illustrate the difference between Mithril and A+ promises, consider the code below:

```javascript
var deferred = m.deferred()
Expand All @@ -102,6 +103,29 @@ 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.

There are a couple of reasons why Mithril runs callbacks synchronously. Conforming to the spec requires either a `setImmediate` polyfill (which is a significantly large library), or `setTimeout` (which is required to take at least 4 milliseconds per call, according to its specs). Neither of these trade-offs are acceptable, given Mithril's focus on nimbleness and performance.

#### Unchecked Error Handling

Mithril does not swallow errors if these errors are subclasses of the Error class. Manually throwing an instance of the Error class itself (or any other objects or primitives) does trigger the rejection callback path as per the Promises/A+ spec.

This deviation from the spec is there to make it easier for developers to find common logical errors such as typos that lead to null reference exceptions. By default, the spec requires that all thrown errors trigger rejection, which result in silent failures if the developer forgets to explicitly handle the failure case.

For example, there is simply never a case where a developer would want to programmatically handle the error of accessing the property of a nullable entity without first checking for its existence. The only reasonable course of action to prevent the potential null reference exceptions in this case is to add the existence check in the source code. It is expected that such an error would bubble up to the console and display a developer-friendly error message and line number there.

The other side of the coin is still supported: if a developer needs to signal an exceptional condition within a promise callback, they can manually throw a `new Error` (for example, if a validation rule failed, and there should be an error message displayed to the user).

---

### Replacing the built-in Promise implementation

If strict adherence to the Promises/A+ spec is required, Mithril allows its built-in implementation to be swapped out. Here's how one would configure Mithril to use the ES6 Promise class that ships with Chrome and Firefox:

```javascript
//use ES6 Promises as the promise engine
m.deferred.constructor = Promise
```
---
### Signature
Expand Down
71 changes: 43 additions & 28 deletions mithril.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Mithril = m = new function app(window, undefined) {
return value
}

function _prop(store) {
function gettersetter(store) {
var prop = function() {
if (arguments.length) store = arguments[0]
return store
Expand All @@ -400,16 +400,15 @@ Mithril = m = new function app(window, undefined) {
return prop
}

m.prop = function (store) {
if ((typeof store === 'object' || typeof store === 'function') && store !== null &&
typeof store.then === 'function') {
var prop = _prop()
m.prop = function(store) {
if ((typeof store == "object" || typeof store == "function") && store !== null && typeof store.then == "function") {
var prop = gettersetter()
newPromisedProp(prop, store).then(prop)

return prop
}

return _prop(store)
return gettersetter(store)
}

var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false
Expand Down Expand Up @@ -589,29 +588,21 @@ Mithril = m = new function app(window, undefined) {
cellCache[cacheKey] = undefined
}

function newPromisedProp(prop, promise) {
prop.then = function () {
var newProp = m.prop()
return newPromisedProp(newProp,
promise.then.apply(promise, arguments).then(newProp))
}
prop.promise = prop
prop.resolve = function (val) {
prop(val)
promise = promise.resolve.apply(promise, arguments)
return prop
}
prop.reject = function () {
promise = promise.reject.apply(promise, arguments)
return prop
m.deferred = function () {
var resolve, reject, executor = function(a, b) {
resolve = a, reject = b
}

return prop
return newPromisedProp(m.prop(), new m.deferred.constructor(executor))
}
m.deferred = function () {
return newPromisedProp(m.prop(), new Deferred())
m.deferred.constructor = function(executor) {
var deferred = new Deferred()
executor(deferred.resolve, deferred.reject)
return deferred
}
// Promiz.mithril.js | Zolmeister | MIT
//Promiz.mithril.js | Zolmeister | MIT
//a modified version of Promiz.js, which does not conform to Promises/A+ for two reasons:
//1) `then` callbacks are called synchronously (because setTimeout is too slow, and the setImmediate polyfill is too big
//2) throwing subclasses of Error cause the error to be bubbled up instead of triggering rejection (because the spec does not account for the important use case of default browser error handling, i.e. message w/ line number)
function Deferred(successCallback, failureCallback) {
var RESOLVING = 1, REJECTING = 2, RESOLVED = 3, REJECTED = 4
var self = this, state = 0, promiseValue = 0, next = []
Expand Down Expand Up @@ -675,6 +666,7 @@ Mithril = m = new function app(window, undefined) {
})
}
catch (e) {
rethrowUnchecked(e)
promiseValue = e
failureCallback()
}
Expand All @@ -690,6 +682,7 @@ Mithril = m = new function app(window, undefined) {
then = promiseValue && promiseValue.then
}
catch (e) {
rethrowUnchecked(e)
promiseValue = e
state = REJECTING
return fire()
Expand All @@ -711,6 +704,7 @@ Mithril = m = new function app(window, undefined) {
}
}
catch (e) {
rethrowUnchecked(e)
promiseValue = e
return finish()
}
Expand All @@ -729,6 +723,28 @@ Mithril = m = new function app(window, undefined) {
})
}
}
function newPromisedProp(prop, promise) {
prop.then = function () {
var newProp = m.prop()
return newPromisedProp(newProp,
promise.then.apply(promise, arguments).then(newProp))
}
prop.promise = prop
prop.resolve = function (val) {
prop(val)
promise = promise.resolve.apply(promise, arguments)
return prop
}
prop.reject = function () {
promise = promise.reject.apply(promise, arguments)
return prop
}

return prop
}
function rethrowUnchecked(e) {
if (type.call(e) == "[object Error]" && !e.constructor.toString().match(/ Error/)) throw e
}

m.sync = function(args) {
var method = "resolve"
Expand Down Expand Up @@ -902,8 +918,7 @@ Mithril = m = new function app(window, undefined) {
}
catch (e) {
if (e instanceof SyntaxError) throw new SyntaxError("Could not parse HTTP response. See http://lhorie.github.io/mithril/mithril.request.html#using-variable-data-formats")
else if (type.call(e) == "[object Error]" && e.constructor !== Error) throw e
else deferred.reject(e)
else if (!rethrowUnchecked(e)) deferred.reject(e)
}
if (xhrOptions.background !== true) m.endComputation()
}
Expand Down
18 changes: 18 additions & 0 deletions tests/mithril-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,24 @@ function testMithril(mock) {
deferred.resolve("test")
return value1 === undefined && value2 instanceof Error
})
test(function() {
//Let unchecked exceptions bubble up in order to allow meaningful error messages in common cases like null reference exceptions due to typos
//An unchecked exception is defined as an object that is a subclass of Error (but not a direct instance of Error itself) - basically anything that can be thrown without an explicit `throw` keyword and that we'd never want to programmatically manipulate. In other words, an unchecked error is one where we only care about its line number and where the only reasonable way to deal with it is to change the buggy source code that caused the error to be thrown in the first place.
//By contrast, a checked exception is defined as anything that is explicitly thrown via the `throw` keyword and that can be programmatically handled, for example to display a validation error message on the UI. If an exception is a subclass of Error for whatever reason, but it is meant to be handled as a checked exception (i.e. follow the rejection rules for A+), it can be rethrown as an instance of Error
//This test tests two implementation details that differ from the Promises/A+ spec:
//1) A+ requires the `then` callback to be called in a different event loop from the resolve call, i.e. it must be asynchronous (this requires a setImmediate polyfill, which cannot be implemented in a reasonable way for Mithril's purpose - the possible polyfills are either too big or too slow)
//2) A+ swallows exceptions in a unrethrowable way, i.e. it's not possible to see default error messages on the console for runtime errors thrown from within a promise chain
var value1, value2, value3
var deferred = m.deferred()
try {
deferred.promise
.then(function(data) {foo.bar.baz}) //throws ReferenceError
.then(function(data) {value1 = 1}, function(data) {value2 = data})
deferred.resolve("test")
}
catch (e) {value3 = e}
return value1 === undefined && value2 === undefined && value3 instanceof ReferenceError
})
test(function() {
var deferred1 = m.deferred()
var deferred2 = m.deferred()
Expand Down

0 comments on commit 36999e6

Please sign in to comment.