Skip to content

Instantly share code, notes, and snippets.

@clausreinke
Created May 7, 2013 21:33
Show Gist options
  • Save clausreinke/5536330 to your computer and use it in GitHub Desktop.
Save clausreinke/5536330 to your computer and use it in GitHub Desktop.
// simple monad-like promises, with examples
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