Isolate m.prop() and m.deferred() implementations (mostly)

This mostly isolates the implementations for both of these. Now, everything
here calls the method itself, not any of the external methods.

Few driveby fixes as well:

1. Git now ignores archive/ again (it's a build artifact, and can be removed
   when updating `master`)
2. Since I had to rewrite most of the Deferred implementation, the new version
   passes one of the skipped tests, so it is now enabled.
This commit is contained in:
impinball 2015-11-20 00:55:04 -05:00
parent edc5dca238
commit d7ef127be2
6 changed files with 170 additions and 137 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
node_modules node_modules
archive

View file

@ -1193,9 +1193,39 @@ void (function (global, factory) { // eslint-disable-line
return prop return prop
} }
function isPromise(object) {
return object != null && (isObject(object) || isFunction(object)) &&
isFunction(object.then)
}
function propify(promise) {
var prop = m.prop()
promise.then(prop)
prop.then = promise.then.bind(promise)
prop.catch = promise.then.bind(promise, undefined)
prop.finally = function (callback) {
function _callback() {
return m.deferred().resolve(callback()).promise
}
return promise.then(function (value) {
return _callback().then(function () {
return value
})
}, function (reason) {
return _callback().then(function () {
throw reason
})
})
}
return prop
}
m.prop = function (store) { m.prop = function (store) {
if ((store != null && isObject(store) || isFunction(store)) && if (isPromise(store)) {
isFunction(store.then)) {
return propify(store) return propify(store)
} else { } else {
return gettersetter(store) return gettersetter(store)
@ -1732,164 +1762,167 @@ void (function (global, factory) { // eslint-disable-line
function Deferred(onSuccess, onFailure) { function Deferred(onSuccess, onFailure) {
var self = this var self = this
var state = 0 var promiseValue
var promiseValue = 0
var next = [] var next = []
var func = push
self.promise = {} function coerce(value, next, error) {
if (isPromise(value)) {
return value.then(function (value) {
coerce(value, next, error)
}, function (e) {
coerce(e, error, error)
})
} else {
return next(promiseValue = value)
}
}
function resolve(deferred) {
deferred.resolve(promiseValue)
}
function reject(deferred) {
deferred.reject(promiseValue)
}
function push(deferred) {
next.push(deferred)
}
function init(promise) {
if (func !== reject) promise(promiseValue)
return promise
}
self.resolve = function (value) { self.resolve = function (value) {
if (!state) { if (func === push) {
promiseValue = value fire(RESOLVING, value, self)
state = RESOLVING
fire()
} }
return this return this
} }
self.reject = function (value) { self.reject = function (value) {
if (!state) { if (func === push) {
promiseValue = value fire(REJECTING, value, self)
state = REJECTING
fire()
} }
return this return this
} }
self.promise.then = function (onSuccess, onFailure) { self.promise = function (value) {
var deferred = new Deferred(onSuccess, onFailure) if (arguments.length) coerce(value, noop, noop)
if (state === RESOLVED) { return func !== reject ? promiseValue : undefined
deferred.resolve(promiseValue)
} else if (state === REJECTED) {
deferred.reject(promiseValue)
} else {
next.push(deferred)
}
return deferred.promise
} }
function finish(type) { self.promise.then = function (onSuccess, onFailure) {
state = type || REJECTED var deferred = new Deferred(onSuccess, onFailure)
forEach(next, function (deferred) { func(deferred)
if (state === RESOLVED) { return init(deferred.promise)
deferred.resolve(promiseValue) }
} else {
deferred.reject(promiseValue) self.promise.catch = function (callback) {
} return self.promise.then(null, callback)
}
self.promise.finally = function (callback) {
function _callback() {
var p = new Deferred().resolve(callback()).promise
if (func !== reject) p(promiseValue)
return p
}
return self.promise.then(function () {
return _callback().then(function () {
return promiseValue
})
}, function () {
_callback().then(function () {
throw promiseValue
})
}) })
} }
function thennable(then, success, fail, notThennable) { function run(callback) {
if (((promiseValue != null && isObject(promiseValue)) || func = callback
isFunction(promiseValue)) && isFunction(then)) { forEach(next, callback)
try { // Clear these (which hold all the extra references)
// count protects against abuse calls from spec checker finish = fire = null // eslint-disable-line no-func-assign
var count = 0 }
then.call(promiseValue, function (value) {
if (count++) return function finish(value, state) {
promiseValue = value coerce(value, function () {
success() run(state === RESOLVED ? resolve : reject)
}, function (value) { }, function () {
if (count++) return run(reject)
promiseValue = value })
fail() }
})
} catch (e) { function doThen(value, deferred) {
m.deferred.onerror(e) // count protects against abuse calls from spec checker
promiseValue = e var count = 0
fail()
} try {
} else { return value.then(function (value) {
notThennable() if (count++) return
fire(RESOLVING, value, deferred)
}, function (value) {
if (count++) return
fire(REJECTING, value, deferred)
})
} catch (e) {
m.deferred.onerror(e)
return fire(REJECTING, e, deferred)
} }
} }
function fire() { function notThennable(value, state, deferred) {
// check if it's a thenable
var then
try { try {
then = promiseValue && promiseValue.then if (state === RESOLVING && isFunction(onSuccess)) {
value = onSuccess(value)
} else if (state === REJECTING && isFunction(onFailure)) {
value = onFailure(value)
state = RESOLVING
}
} catch (e) { } catch (e) {
m.deferred.onerror(e) m.deferred.onerror(e)
promiseValue = e return finish(e, REJECTED)
state = REJECTING }
return fire()
if (value === deferred) {
return finish(TypeError(), REJECTED)
} else {
return finish(value, state === RESOLVING ? RESOLVED : REJECTED)
}
}
function fire(state, value, deferred) {
// check if it's a thenable
var thenable
try {
thenable = isPromise(value)
} catch (e) {
m.deferred.onerror(e)
return fire(REJECTING, e, deferred)
} }
if (state === REJECTING) { if (state === REJECTING) {
m.deferred.onerror(promiseValue) m.deferred.onerror(value)
} }
thennable(then, function () { if (thenable) {
state = RESOLVING return doThen(value, deferred)
fire() } else {
}, function () { return notThennable(value, state, deferred)
state = REJECTING }
fire()
}, function () {
try {
if (state === RESOLVING && isFunction(onSuccess)) {
promiseValue = onSuccess(promiseValue)
} else if (state === REJECTING && isFunction(onFailure)) {
promiseValue = onFailure(promiseValue)
state = RESOLVING
}
} catch (e) {
m.deferred.onerror(e)
promiseValue = e
return finish()
}
if (promiseValue === self) {
promiseValue = TypeError()
finish()
} else {
thennable(then, function () {
finish(RESOLVED)
}, finish, function () {
finish(state === RESOLVING && RESOLVED)
})
}
})
} }
} }
m.deferred = function () { m.deferred = function () {
var deferred = new Deferred() return new Deferred()
deferred.promise = propify(deferred.promise)
return deferred
} }
function propify(promise, initialValue) { m.deferred.prototype = Deferred.prototype
var prop = m.prop(initialValue) m.deferred.prototype.constructor = m.deferred
promise.then(prop)
prop.then = function (resolve, reject) {
return propify(promise.then(resolve, reject), initialValue)
}
prop.catch = prop.then.bind(null, null)
prop.finally = function (callback) {
function _callback() {
return m.deferred().resolve(callback()).promise
}
return prop.then(function (value) {
return propify(_callback().then(function () {
return value
}), initialValue)
}, function (reason) {
return propify(_callback().then(function () {
throw new Error(reason)
}), initialValue)
})
}
return prop
}
function isNativeError(e) { function isNativeError(e) {
return e instanceof EvalError || return e instanceof EvalError ||
@ -1908,7 +1941,7 @@ void (function (global, factory) { // eslint-disable-line
} }
m.sync = function (args) { m.sync = function (args) {
var deferred = m.deferred() var deferred = new Deferred()
var outstanding = args.length var outstanding = args.length
var results = new Array(outstanding) var results = new Array(outstanding)
var method = "resolve" var method = "resolve"
@ -2077,7 +2110,6 @@ void (function (global, factory) { // eslint-disable-line
m.request = function (options) { m.request = function (options) {
if (options.background !== true) m.startComputation() if (options.background !== true) m.startComputation()
var deferred = new Deferred() var deferred = new Deferred()
var serialize = identity var serialize = identity
@ -2127,7 +2159,6 @@ void (function (global, factory) { // eslint-disable-line
} }
} }
} catch (e) { } catch (e) {
m.deferred.onerror(e)
response = e response = e
doSuccess = false doSuccess = false
} }
@ -2138,7 +2169,7 @@ void (function (global, factory) { // eslint-disable-line
} }
ajax(options) ajax(options)
deferred.promise = propify(deferred.promise, options.initialValue) deferred.promise(options.initialValue)
return deferred.promise return deferred.promise
} }

6
mithril.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -117,8 +117,7 @@ describe("m.deferred()", function () {
expect(value2()).to.be.an("error") expect(value2()).to.be.an("error")
}) })
// FIXME: this is a bug. it("synchronously throws subclasses of Errors on creation", function () {
xit("synchronously throws subclasses of Errors on creation", function () {
expect(function () { expect(function () {
m.deferred().reject(new TypeError()) m.deferred().reject(new TypeError())
}).to.throw() }).to.throw()

View file

@ -53,12 +53,14 @@ describe("m.prop()", function () {
it("returns a thenable when wrapping a Mithril promise", function () { it("returns a thenable when wrapping a Mithril promise", function () {
var defer = m.deferred() var defer = m.deferred()
var prop = m.prop(defer.promise).then(function () { var prop = m.prop(defer.promise)
var promise = prop.then(function () {
return "test2" return "test2"
}) })
defer.resolve("test") defer.resolve("test")
expect(prop()).to.equal("test2") expect(promise()).to.equal("test2")
}) })
}) })