Created
May 7, 2013 21:33
-
-
Save clausreinke/5536330 to your computer and use it in GitHub Desktop.
// simple monad-like promises, with examples
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare function setImmediate(cb); | |
declare function setTimeout(cb,n); | |
declare var process; | |
function nextTick(cb) { | |
if (typeof setImmediate == "function") { | |
setImmediate(cb); | |
} else if (typeof setTimeout == "function") { | |
setTimeout(cb,0); | |
} else if (typeof process == "object" && typeof process.nextTick == "function") { | |
process.nextTick(cb); | |
} else { | |
throw "no nextTick"; | |
} | |
} | |
// simple monad-like promises, | |
// with synchronous and asynchronous resolution, rejection and mock operations; | |
// works with tsc (via npm) or http://www.typescriptlang.org/playground/ (TS v0.8.3) | |
// claus reinke, 2013 | |
/* abstract */ | |
class Promise { | |
then(cb:(value)=>Promise):Promise { | |
throw "abstract then"; | |
}; | |
then_(cb:(value)=>any):Promise { // result-wrapping helper | |
return this.then( value => this["constructor"].of( cb(value) ) ); | |
}; | |
static of(value): Promise { | |
throw "abstract of"; | |
}; | |
static raise(error): Promise { | |
throw "abstract throw"; | |
} | |
catch(cb:(error)=>Promise):Promise { | |
throw "abstract catch"; | |
} | |
catch_(cb:(error)=>any):Promise { // result-wrapping helper | |
return this.catch( error => this["constructor"].of( cb(error) ) ); | |
}; | |
} | |
class ResolvedPromise extends Promise { | |
constructor(private value) { | |
super(); | |
} | |
then(cb:(value)=>Promise):Promise { | |
return wrap(cb)(this.value) | |
} | |
static of(value):Promise { | |
return <Promise>new ResolvedPromise(value); | |
} | |
static raise(error): Promise { | |
return <Promise>new RejectedPromise(error); | |
} | |
catch(cb:(error)=>Promise):Promise { | |
return this; | |
} | |
} | |
class RejectedPromise extends Promise { | |
constructor(private error) { | |
super(); | |
} | |
then(cb:(value)=>Promise):Promise { | |
return this; | |
} | |
static of(value):Promise { | |
return <Promise>new ResolvedPromise(value) | |
} | |
static raise(error): Promise { | |
return <Promise>new RejectedPromise(error); | |
} | |
catch(cb:(error)=>Promise):Promise { | |
return wrap(cb)(this.error); | |
} | |
} | |
class PendingPromise extends Promise { | |
constructor( private resolver:Resolver ) { | |
super(); | |
} | |
then(cb:(value)=>Promise):Promise { | |
return this.resolver.handle(cb,null); | |
} | |
static of(value):Promise { | |
return <Promise>new ResolvedPromise(value); | |
} | |
static raise(error): Promise { | |
return <Promise>new RejectedPromise(error); | |
} | |
catch(cb:(error)=>Promise):Promise { | |
return this.resolver.handle(null,cb); | |
} | |
} | |
class Resolver { | |
private listeners = []; | |
public promise; | |
private resolved = false; | |
private value = undefined; | |
private error = undefined; | |
constructor() { | |
this.promise = new PendingPromise(this); | |
} | |
handle(cb:(value)=>Promise, err:(error)=>Promise):Promise { | |
if (typeof this.value != "undefined") { | |
return cb ? wrap(cb)(this.value) : this.promise; | |
} else if (typeof this.error != "undefined") { | |
return err ? wrap(err)(this.error) : this.promise; | |
} else { | |
var r = new Resolver(); | |
var resolve = vn => ResolvedPromise.of( r.resolve( vn, true ) ); | |
var reject = e => RejectedPromise.raise( r.reject( e, true ) ); | |
if (cb) { | |
this.listeners.push( p => | |
p.then(wrap(cb)).then( resolve ).catch( reject ) | |
); | |
} else if (err) { | |
this.listeners.push( p => | |
p.catch(wrap(err)).then( resolve ).catch( reject ) | |
); | |
} else { | |
throw "handle called without success or error callback"; | |
} | |
return r.promise; | |
} | |
} | |
resolve(value,sync=false) { | |
if (this.resolved) throw "already resolved!"; | |
var p = ResolvedPromise.of(value); | |
if (sync) { | |
this.resolved = true; | |
this.value = value; | |
this.listeners.forEach( l => l(p) ); | |
this.listeners = null; | |
} else { | |
this.resolved = true; // avoid resolve conflicts .. | |
nextTick(()=>{ | |
this.value = value; // .. but don't make value available early | |
this.listeners.forEach( l => l(p) ); | |
this.listeners = null; | |
}); | |
} | |
} | |
reject(error,sync=false) { | |
if (this.resolved) throw "already resolved!"; | |
var p = RejectedPromise.raise(error); | |
if (sync) { | |
this.resolved = true; | |
this.error = error; | |
this.listeners.forEach( l => l(p) ); | |
this.listeners = null; | |
} else { | |
this.resolved = true; // avoid resolve conflicts .. | |
nextTick(()=>{ | |
this.error = error; // .. but don't make error available early | |
this.listeners.forEach( l => l(p) ); | |
this.listeners = null; | |
}); | |
} | |
} | |
} | |
function wrap(cb) { // redirect exceptions to rejections | |
return function(value) { | |
try { | |
return cb(value); | |
} catch (e) { | |
return RejectedPromise.raise(e); | |
} | |
} | |
} | |
// synchronous and asynchronous dummy operations, | |
// with labeled trace output | |
var syncOp = prefix => label => value => ( | |
console.log(prefix,label+"(sync)",value), | |
ResolvedPromise.of(label.toLowerCase()) | |
); | |
var asyncOp = prefix => label => value => { | |
var r = new Resolver(); | |
nextTick(()=>( console.log(prefix,label+"(async)",value), | |
r.resolve(label.toLowerCase()) | |
)); | |
return r.promise; | |
}; | |
function test(label,prefix,sync,op) { | |
console.log("// "+label +" (prefix "+prefix+")"); | |
var rA = new Resolver(); | |
var pA = rA.promise; | |
var pB = pA.then( op(prefix)("B") ); | |
var pC = pA.then( syncOp(prefix)("C") ); | |
var pD = pB.then( op(prefix)("D") ); | |
var pE = pB.then( syncOp(prefix)("E") ); | |
var pF = pC.then( op(prefix)("F") ); | |
var pG = pC.then( syncOp(prefix)("G") ); | |
var pH = pA.then( op(prefix)("H") ); | |
var pI = pA.then( syncOp(prefix)("I") ); | |
pH.then_( x => x ).then_( x => console.log(prefix,x) ); | |
console.log(prefix,1); | |
rA.resolve("a",sync); | |
console.log(prefix,2); | |
} | |
test("sync resolve, syncOp", ".", true, syncOp); | |
test("async resolve, asyncOp", "..", false, asyncOp); | |
console.log("// resolved pipeline (prefix |)"); | |
ResolvedPromise.of(1).then( x => { throw "oops" } ) | |
.then_( x => console.log("|result:",x) ) | |
.catch_( e => console.log("|error:",e) ) | |
.then_( x => console.log("|result:",x) ) | |
.catch_( e => console.log("|error:",e) ); | |
console.log("// nested resolved pipeline (prefix ||)"); | |
ResolvedPromise.of(ResolvedPromise.of("scary nested thing")) | |
.then_( p => ( console.log("||",p) , p ) ) | |
.then( p => p ) | |
.then_( v => console.log("||","no "+v) ); | |
var nested = x => { | |
var r1 = new Resolver(); | |
nextTick( () => { var r2 = new Resolver(); | |
r1.resolve( r2.promise ) | |
nextTick( () => r2.resolve( x ) ); | |
} ); | |
return r1.promise; | |
}; | |
console.log("// nested async pipeline (prefix >)"); | |
nested(42).then_( p => { console.log(">","react to outer promise level"); | |
p.then_( x => console.log(">","react to inner level",x) ); | |
console.log(">","now waiting for nested promise") | |
}); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment