Skip to content

Instantly share code, notes, and snippets.

@XEngine
Forked from getify/1.md
Created May 28, 2018 23:29
Show Gist options
  • Save XEngine/e61d16a3d927399c44b408da8d8a37b8 to your computer and use it in GitHub Desktop.
Save XEngine/e61d16a3d927399c44b408da8d8a37b8 to your computer and use it in GitHub Desktop.
BetterPromise: a strawman experiment in subclassing Promise and "fixing" a bunch of its awkward/bad parts

Some things that are "better" with this BetterPromise implementation:

  • BetterPromise # unthen(..) / BetterPromise # uncatch(..) / BetterPromise # unfinally(..) allows you to unregister a fulfillment or rejection handler that you previously registered on a promise via then(..) or catch(..), or unregister a resolution handler that you previously registered on a promise via finally(..).

    NOTE: This seems to be the majority use-case for why many like/want "promise cancelation" -- IOW, often what you want is to just stop observing a promise's resolution, not actually forcibly cancel the operation is comes from.

    var p = new BetterPromise(function(res){
       setTimeout(function(){ res(42); },100);
    });
    
    function f1(v) { console.log(`f1: ${v}`); }
    function f2(v) { console.log(`f2: ${v}`); }
    function f3() { console.log("finally!"); }
    
    p.then(f1);
    p.then(f2);
    p.finally(f3);
    p.unthen(f1);
    p.unfinally(f3);
    // later
    // f2: 42
  • BetterPromise # finally(..) is included (assumed implemented since it's already stage-4). Registers a resolution handler which is called on either fulfillment or rejection, sorta like if you did then( fn, fn ) (but not exactly).

    var p = BetterPromise.resolve(42);
    p
       .finally(function(){ console.log("resolved!"); })
       .then(function(v){ console.log(`still: ${v}`); });
    // resolved!
    // still: 42
  • BetterPromise # thenLog() / BetterPromise # catchLog() inserts a step in a promise chain that simply prints the value to the console (console.log for fulfillment, console.error for rejection) and passes the value or rejection along to the next step untouched.

    var p = BetterPromise.resolve(42);
    p
      .thenLog()
      .then(function(v){ console.log(`still: ${v}`); });
    // 42
    // still: 42
  • Instead of silently being swallowed, if a synchronous exception is thrown in a BetterPromise constructor after the promise has already been synchronously resolved (fulfillment or rejection), that exception overrides and causes the promise to be rejected with that exception.

    var p = new BetterPromise(function(res){
       res(42);
       throw 10;
    });
    
    p.then(
       function(v){ console.log(`then: ${v}`); },
       function(e){ console.log(`oops: ${e}`); }
    );
    // oops: 10
  • BetterPromise.try(..) (static helper) is implemented (so not just draft). Runs a function (with no arguments) synchronously, returns a promise for its return value (or adopts its promise), catches any synchronous exception and turns it into a rejection.

    var p = BetterPromise.try(function(){
       undefined(42);
    });
    p.catch(console.log);
    // TypeError: undefined is not a function
  • BetterPromise.deferred(..) (static helper) constructs an instance of the promise, but also extracts its resolve(..) and reject(..) capabilities, and returns all 3 in an object (aka, a "deferred").

    var { pr, resolve, reject } = BetterPromise.deferred();
    
    pr.then(console.log);
    resolve(42);
    // 42
  • BetterPromise.lift(..) (static helper) lifts an error-first, callback-last style function to be BetterPromise returning instead.

    var readFile = BetterPromise.lift(fs.readFile);
    readFile("/path/to/file.txt")
    .then(printContents);
  • BetterPromise.control(..) (static helper) wraps a function so that when called, it first creates an AbortController instance, passes in its signal as the first argument to the original function, and returns a controller object that has both a pr for the function's completion, as well as a cancel(..) to send the abort signal. Ostensibly, the original function can then observe/respond to that passed-in signal and do something appropriate with it, like canceling its own behavior, passing it to fetch(..) to abort an Ajax call, etc.

    async function main(signal,url) {
       signal.addEventListener("abort", .. );
    
       // ..
                                 
       var resp = await fetch(url, { signal });
                                     
       // ..
    }
    
    var fn = BetterPromise.control(main);
    var { pr, cancel } = fn("http://some.url");
    
    pr.then(..);
    
    // later
    cancel();  // sends cancelation signal into `fn(..)`
class BetterPromise extends Promise {
static resolve(v) {
return new this[Symbol.species](function c(res){
res(v);
});
}
static reject(v) {
return new this[Symbol.species](function c(res,rej){
rej(v);
});
}
static try(fn) {
return new this[Symbol.species](function c(res){
res(fn());
});
}
static deferred() {
var resolve;
var reject;
var pr = new this[Symbol.species](function c(res,rej){
resolve = res;
reject = rej;
});
return { resolve, reject, pr, };
}
static lift(fn) {
return (...args) => {
return new this[Symbol.species](function c(res,rej){
fn(...args,function cb(err,any){
if (err) rej(err);
else res(any);
});
});
};
}
static control(fn) {
return (...args) => {
var token = new AbortController();
var pr = this[Symbol.species].try(function c(){
return fn(token.signal,...args);
});
return { pr, cancel: token.abort.bind(token), };
};
}
constructor(fn) {
super(function c(res,rej){
var resolution;
var rejection;
var syncComplete = false;
var resolveCalled = false;
var rejectCalled = false;
try {
fn(
function resolve(v){
if (!syncComplete) {
resolveCalled = true;
resolution = v;
}
else {
res(v);
}
},
function reject(e){
if (!syncComplete) {
rejectCalled = true;
rejection = e;
}
else {
rej(e);
}
}
);
syncComplete = true;
if (resolveCalled) res(resolution);
else if (rejectCalled) rej(rejection);
}
catch (err) {
rej(err);
}
});
this.__fulfilled_handlers = new WeakMap();
this.__rejected_handlers = new WeakMap();
this.__resolved_handlers = new WeakMap();
}
then(origOnFulfilled,origOnRejected) {
var thenArgs = [];
if (typeof origOnFulfilled == "function") {
let onFulfilled = (...args) => {
if (this.__fulfilled_handlers.has(origOnFulfilled)) {
return origOnFulfilled(...args);
}
return args[0];
};
this.__fulfilled_handlers.set(origOnFulfilled,true);
thenArgs.push(onFulfilled);
}
else {
thenArgs.push(undefined);
}
if (typeof origOnRejected == "function") {
let onRejected = (...args) => {
if (this.__rejected_handlers.has(origOnRejected)) {
return origOnRejected(...args);
}
return args[0];
};
this.__rejected_handlers.set(origOnRejected,true);
thenArgs.push(onRejected);
}
else {
thenArgs.push(undefined);
}
return super.then(...thenArgs);
}
catch(origOnRejected) {
return this.then(undefined,origOnRejected);
}
finally(origOnResolved) {
if (typeof origOnResolved == "function") {
let onResolved = () => {
if (this.__resolved_handlers.has(origOnResolved)) {
return origOnResolved();
}
};
this.__resolved_handlers.set(origOnResolved,true);
return super.finally(onResolved);
}
else {
return super.finally();
}
}
thenLog() {
return this.then(function then(v){
console.log(v);
return v;
});
}
catchLog() {
return this.catch(e => {
console.error(e);
return this.constructor[Symbol.species].reject(e);
});
}
unthen(origOnFulfilled,origOnRejected) {
if (typeof origOnFulfilled == "function") {
if (this.__fulfilled_handlers.has(origOnFulfilled)) {
this.__fulfilled_handlers.delete(origOnFulfilled);
}
}
if (typeof origOnRejected == "function") {
if (this.__rejected_handlers.has(origOnRejected)) {
this.__rejected_handlers.delete(origOnRejected);
}
}
return this;
}
uncatch(origOnRejected) {
return unthen(undefined,origOnRejected);
}
unfinally(origOnResolved) {
if (typeof origOnResolved == "function") {
if (this.__resolved_handlers.has(origOnResolved)) {
this.__resolved_handlers.delete(origOnResolved);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment