Skip to content

Instantly share code, notes, and snippets.

@KiaraGrouwstra
Created August 21, 2016 16:00
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KiaraGrouwstra/ac9c024e0996fda7c4d1290241e26277 to your computer and use it in GitHub Desktop.
Save KiaraGrouwstra/ac9c024e0996fda7c4d1290241e26277 to your computer and use it in GitHub Desktop.
using ES6 Proxy to deal with methods of Promise'd objects. not sure how useful this is yet.
// using ES6 Proxy to deal with methods of Promise'd objects. works for me in Edge though not Chrome somehow.
let handler = {
get: (target, prop) => function() {
if(target instanceof Promise) {
let args = arguments;
return target.then((o) => o[prop].apply(o, args));
} else {
let value = target[prop];
return typeof value == 'function' ? value.bind(target) : value;
}
}
};
let obj = { greet: (name) => console.log('Hey ' + name) };
let later = (v) => new Promise((resolve, reject) => setTimeout(() => resolve(v), 1000))
let prom = later(obj);
let greeter = new Proxy(prom, handler);
greeter.greet('you');
@sjkillen
Copy link

Neat, I just wrote some very similar code (https://gist.github.com/Spensaur-K/5c1b8d32e5bfc8d5863778e2976f4799)

I find it odd that functions need to be bound to the target before being returned
(new Proxy(Promise.resolve(5), {})).then()
Generates a complaint for something I feel should be fine.

@anodynos
Copy link

anodynos commented Mar 24, 2020

Thanks! Let me add my PoC, that returns a Proxy of the original promise that projects the resolved value (i.e the value passed to your .then(value => // do things with value)

const projectPromiseValueProxy = (aPromise, projectTo) =>
  new Proxy(aPromise, {
    get: (targetPromise, prop) => {
      if (prop === 'then') {
        const originalThen = targetPromise.then.bind(targetPromise);

        const newThen = (function (thenFun) {
          return originalThen(value => thenFun(projectTo(value)));
        }).bind(targetPromise);

        return newThen;
      }

      return targetPromise[prop];
    }
  });

const promise = new Promise(resolve => setTimeout(() => resolve({ body: 42 }), 1000));

const projectedPromise = projectPromiseValueProxy(promise, (value) => value.body * 10);

projectedPromise
  .then(response => {
    console.log(response); // prints 420
    return { whatever: response };
  })
  .then(response => console.log(response)) // prints {whatever: 420}
  .catch(console.error);

@anodynos
Copy link

anodynos commented Mar 25, 2020

A slightly more involved version, if .finally or .catch are used before the first .then, we need to proxy those promises too:
(edit: CAVEAT: this version actually throws UnhandledPromiseRejectionWarning with rejected promises, if .catch preceeds .then - scroll below for the one that works!)

const projectPromiseValueProxy = (aPromise, projectTo) =>
  new Proxy(aPromise, {
    get: (targetPromise, prop) => {

      // project value of the first .then
      if (prop === 'then') {
        const originalThen = targetPromise.then.bind(targetPromise);

        const newThen = (function (userThenFunction) {
          return originalThen(value => userThenFunction(projectTo(value)));
        }).bind(targetPromise);

        return newThen;
      }

      // when we have `promise.catch().finally().then()` (i.e finally/catch before first .then)
      // we need to proxy until we find our first .then
      if (prop === 'finally' || prop === 'catch') {
        const originalFinallyOrCatch = targetPromise[prop].bind(targetPromise);

        const newFinallyOrCatch = (function (userFinallyFunction) {
          return projectPromiseValueProxy(originalFinallyOrCatch(userFinallyFunction), projectTo);
        }).bind(targetPromise);

        return newFinallyOrCatch;
      }

      return targetPromise[prop];
    },
  });

const promise = new Promise(resolve => setTimeout(() => resolve({ body: 42 }), 1000));

const projectedPromise = projectPromiseValueProxy(promise, (value) => value.body * 10);

projectedPromise
  .catch(console.error)
  .then(response => {
    console.log(response); // prints 420
    return { whatever: response };
  })
  .then(response => console.log(response)) // prints {whatever: 420} (i.e 2nd then its not proxied at all)
  .finally(() => console.log('finito'))

@KiaraGrouwstra
Copy link
Author

so this is about retaining proxies across Promise chains right? that's kinda cool. :)

@anodynos
Copy link

Thanks Kiarra! Yeah, its about projecting the value of the original promise to something else (i.e pass the value through projectTo when its resolved).

@anodynos
Copy link

anodynos commented Mar 25, 2020

This version works with rejected promises as well - its much less verbose also :-)

// the "main" function
const projectPromiseValueProxy = (aPromise, projectTo) =>
  new Proxy(aPromise, {
    get: (targetPromise, prop) => {

      // project value of the first .then (only the first makes sense!)
      if (prop === 'then')
        return (userThenFunction) => targetPromise.then(value => userThenFunction(projectTo(value)))

      // when we have `promise.catch().finally().then()` (i.e finally/catch before first .then)
      // we need to proxy until we find our first .then
      if (prop === 'finally' || prop === 'catch')
        return (userFinallyOrCatchFunction) =>
          projectPromiseValueProxy(targetPromise[prop](userFinallyOrCatchFunction), projectTo);

      return targetPromise[prop];
    },
  });

// examples: 

// resolving promise
const promise = new Promise(resolve => setTimeout(() => resolve({body: 42}), 1000));
const projectedPromise = projectPromiseValueProxy(promise, (value) => value?.body * 10);

projectedPromise
.catch(console.error)
.then(response => {
  console.log(response); // prints 420
  return {whatever: response};
})
.then(response => console.log(response)) // prints {whatever: 420} (i.e 2nd then its not proxied at all)
.finally(() => console.log('finito'))

// rejecting promise
// const rejectingPromise = Promise.reject('Shit !!!')
const rejectingPromise = new Promise((resolve, reject) => setTimeout(()=> reject('Shiiiiiiit!'), 2000))
const projectedRejectingPromise = projectPromiseValueProxy(rejectingPromise, (value) => value?.body * 10);

projectedRejectingPromise
  .catch(err => console.error('rejectingPromise rejected:', err))
  .then(response => {
    console.log(response); // prints 420
    return {whatever: response};
  });

// rejecting promise, with try-catch block
(async () => {
  try {
    await projectedRejectingPromise // if you omit await here, you get `UnhandledPromiseRejectionWarning`
    .then(response => {
      console.log(response); // prints 420
      return {whatever: response};
    })
  } catch (error) {
    console.error('rejectingPromise try-catch :', error)
  }
})();

See it in action https://jsbin.com/soxotad/edit?js,output

@KiaraGrouwstra
Copy link
Author

I'm curious now, did you have any particular use-case in mind with that? or just PoC like mine?

@anodynos
Copy link

Sure, we wanted to convert our request-promise requests from using resolveWithFullResponse so we needed to extract body from all results, as in the example above, without touching every single request...

In the end some tests were still failing with UnhandledPromiseRejectionWarning, still not sure why.. so we had to backtrack from it... Interesting lesson though!

@KiaraGrouwstra
Copy link
Author

ohh cool I see, thanks! :)

@bekharsky
Copy link

I can say for sure, this can be helpful for "promisified" libraries. In our project I promisified google.script.run, to be able to work with Google Apps Script client code via promises and react-query. But mocking became like hell.

This kind of proxy wrapper allows me to mock the data during development without launching project in Sheets.

Thanks!

@anodynos
Copy link

@bekharsky thanks - I know my code had caused us some issues back then (so we backtracked from it), please do let us know if you have encountered any issues and please post us any updated code if you fix them ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment