jean d'arme
jean d'arme

Reputation: 4343

Function wrapper for another functions

I have found that a lot of my API calls functions change loading property to true in the beginning and to false after it's finished. The thing is I have a lot of functions and I like to keep my code DRY.

So, I came up with something like this:

async loadingWrap (func, ...args) {
  this.loading = true

  await func(...args)

  this.loading = false
}

and when I call it is like this:

await this.loadingWrap(
  this.someAsyncFunction, { param: 'Value' }
)

where what ideally I want would be:

await this.loadingWrap(this.someAsyncFunction({ param: 'Value'}))

so it will look like a normal function to the future reader (me or someone else).

Is that possible? I looked at higher-order functions, but no luck so far.

Upvotes: 1

Views: 2593

Answers (5)

TheJim01
TheJim01

Reputation: 8866

From what you want:

await this.loadingWrap(this.someAsyncFunction({ param: 'Value'}))

This won't work because it will treat the parameter as a nested function. The order of operations will be:

  1. Call this.someAsyncFunction({ param: 'Value'})
  2. Call this.loadingWrap(x) where x is the return value of step 1

This type of function evaluation is exactly like mathematical functions, where to evaluate f(g(x)) (f of g, given x), you first evaluate g given the value x, and then use the result to evaluate f.

A possible solution...

You might be able to use JavaScript's Proxy object. As the docs say, you can use them on a function by using the apply trap.

You'll write your handler generically to handle any function trying to use a loading flag.

const handler = {
  apply: async function(target, thisArg, argumentsList) {
    thisArg.loading = true
    await target.apply(thisArg, argumentsList)
    thisArg.loading = false
  }
}

You will then create your someAsyncFunction member function by creating the proxy like this:

YourClass.prototype.someAsyncFunction = new Proxy(someAsyncFunction, handler);

Then you call it like this:

// inside some other async member function...
  await this.someAsyncFunction({ param: 'Value'})

Here is a run-able example (there's nothing on the page, just console output):

class MyObj {
  constructor() {
    this.loading = false
  }

  async someAsyncFunction(val) {
    console.log(`entering someAsyncFunction: loading = ${this.loading}`)
    console.log(`calling this.asyncLoad...`)
    await this.asyncLoad({
      value: val
    })
    console.log(`exiting someAsyncFunction: loading = ${this.loading}`)
  }
}

async function asyncLoad(params) {
  return new Promise(resolve => {
    console.log(`entering asyncLoad: loading = ${this.loading}, value = ${params.value}`)
    setTimeout(() => {
      console.log(`exiting asyncLoad: loading = ${this.loading}, value = ${params.value}`)
      resolve()
    }, 1000)
  })
}

const handler = {
  apply: async function(target, thisArg, argumentsList) {
    console.log('PROXY: setting load to true...')
    thisArg.loading = true
    console.log('PROXY: calling the proxied function...')
    await target.apply(thisArg, argumentsList)
    console.log('PROXY: setting load to false...')
    thisArg.loading = false
  }
}

MyObj.prototype.asyncLoad = new Proxy(asyncLoad, handler);

async function run() {
  let myobj = new MyObj()
  console.log(`in run, before calling someAsyncFunction, loading = ${myobj.loading}`)
  setTimeout(() => {
    console.log(`INTERRUPT: checking loading is true (${myobj.loading})`)
  }, 500)
  await myobj.someAsyncFunction(1)
  console.log(`in run, after calling someAsyncFunction, loading = ${myobj.loading}`)
}
run()

Selective Proxy-ing

If the function you're trying to call is generic enough that you only need to perform Proxy actions sometimes, this is entirely do-able. This is also where Proxy becomes really cool, because you can create different proxies to perform different actions while maintaining the same base code.

In the example below, asyncLoad is my generic function, and I can call it providing an instance of ObjWithoutStatus as the function's this context. But I also created two proxies, one to set the loading status, and another to set the loaderIsRunning status. Each of these end up calling the base function, without having to perform the gymnastics of creating wrappers that maintain the correct scope.

class ObjWithoutStatus {
  constructor() {}
}

class ObjWithLoading {
  constructor() {
    this.loading = false
  }
}

class ObjWithLoaderIsRunning {
  constructor() {
    this.loaderIsRunning = false
  }
}

async function asyncLoad(params) {
  return new Promise(resolve => {
    console.log(`entering asyncLoad: loading = ${this.loading}, value = ${params.value}`)
    setTimeout(() => {
      console.log(`exiting asyncLoad: loading = ${this.loading}, value = ${params.value}`)
      resolve()
    }, 1000)
  })
}

const handler_loading = {
  apply: async function(target, thisArg, argumentsList) {
    console.log('PROXY_loading: setting load to true...')
    thisArg.loading = true
    console.log('PROXY_loading: calling the proxied function...')
    await target.apply(thisArg, argumentsList)
    console.log('PROXY_loading: setting load to false...')
    thisArg.loading = false
  }
}

const handler_loaderIsRunning = {
  apply: async function(target, thisArg, argumentsList) {
    console.log('PROXY_loaderIsRunning: setting load to true...')
    thisArg.loaderIsRunning = true
    console.log('PROXY_loaderIsRunning: calling the proxied function...')
    await target.apply(thisArg, argumentsList)
    console.log('PROXY_loaderIsRunning: setting load to false...')
    thisArg.loaderIsRunning = false
  }
}

const asyncLoad_loading = new Proxy(asyncLoad, handler_loading)
const asyncLoad_loaderIsRunning = new Proxy(asyncLoad, handler_loaderIsRunning)

const x = new ObjWithoutStatus()
const y = new ObjWithLoading()
const z = new ObjWithLoaderIsRunning()

async function run() {

  console.log(`in run, before calling asyncLoad, x.loading, x.loaderIsRunning = ${x.loading}, ${x.loaderIsRunning}`)
  setTimeout(() => console.log(`INTERRUPT_asyncLoad: x.loading, x.loaderIsRunning = ${x.loading}, ${x.loaderIsRunning}`), 500)
  await asyncLoad.call(x, {
    value: 1
  })
  console.log(`in run, after calling asyncLoad, x.loading, x.loaderIsRunning = ${x.loading}, ${x.loaderIsRunning}`)

  console.log(`in run, before calling asyncLoad_loading, y.loading = ${y.loading}`)
  setTimeout(() => console.log(`INTERRUPT_asyncLoad_loading: y.loading = ${y.loading}`), 500)
  await asyncLoad_loading.call(y, {
    value: 2
  })
  console.log(`in run, after calling asyncLoad_loading, y.loading = ${y.loading}`)

  console.log(`in run, before calling asyncLoad_loaderIsRunning, z.loaderIsRunning = ${z.loaderIsRunning}`)
  setTimeout(() => console.log(`INTERRUPT_asyncLoad_loading: z.loaderIsRunning = ${z.loaderIsRunning}`), 500)
  await asyncLoad_loaderIsRunning.call(z, {
    value: 3
  })
  console.log(`in run, after calling asyncLoad_loaderIsRunning, z.loaderIsRunning = ${z.loaderIsRunning}`)

}
run()

Upvotes: 1

slebetman
slebetman

Reputation: 113866

You can almost get what you want. What you need to do is to not pass the arguments and call the function without any arguments. This behaves similarly to native functions that accept callbacks like setTimeout() or addEventListener():

async loadingWrap (func) {
  this.loading = true

  await func()

  this.loading = false
}

Then call it similar to how you'd call functions like setTimeout():

await this.loadingWrap(() => this.someAsyncFunction({ param: 'Value'}))

The trick is to wrap your function in an anonymous function that accepts no arguments - just like other functions like it in the js world.

Here's a full working demo with console.log replacing the loading variable:

async function loadingWrap (func) {
  console.log('loading');
  
  await func()
    
  console.log('done loading');
}

function timer (x) {
  return new Promise((ok,fail) => setTimeout(ok,x));
}

async function test () {
  console.log('calling async function');

  await loadingWrap(() => timer(2000));

  console.log('finished calling async function');
}

test();

Upvotes: 1

frodo2975
frodo2975

Reputation: 11725

You're looking for a higher order function, which is just a function that returns a function. Lodash uses techniques like this for functions like throttle or debounce.

// Wrap your function in another function that sets the loading property.
// We're passing "this" as "that" to preserve it when loadStuff is called.
function loadingWrap(that, functionToWrap) {
    return async function() {
        that.loading = true;
        let returnVal = await functionToWrap.apply(that, arguments);
        that.loading = false;
        return returnVal;
    }
}

// In your API class
public loadStuff1 = loadingWrap(this, (arg1, arg2) => {
    // This method must return a promise for the loading wrap to work.
    return http.get('someURL', arg1, arg2);
});

// In the class that uses your api
myAPI.loadStuff1('abc', 123);

Upvotes: 0

Mark  Partola
Mark Partola

Reputation: 674

Consider using wrapper like this:

function bar(fn) {
  console.log('before');
  fn();
  console.log('after');
}

function baz(...params) {
  console.log('inside', params);
}

bar(() => baz(1, 2, 3));

class A {
  constructor() {
    this.loading = false;
  }

  async loadable(fn) {
      this.loading = true;
      await fn();
      this.loading = false;
  }

  async load() {
    return new Promise(res => setTimeout(res, 2000))
  }

  async fetch() {
    this.loadable(this.load); // or () => this.load(params)
  }
}

new A().fetch();

Upvotes: -1

trincot
trincot

Reputation: 350147

It is possible, but you need to make sure the this value is correctly set when the actual function is called:

async loadingWrap (func, thisArg, ...args) {
  this.loading = true;
  // support functions that resolve to something useful. And provide `this`
  let result = await func.apply(thisArg, args);
  this.loading = false
  return result; 
}

And in some async method:

  let result = await this.loadingWrap(
    this.someAsyncFunction, this, { param: 'Value' }
  );
  console.log(result);

If you don't like the extra parameter, then you must pass a callback function that sets the this binding correctly, and then you might as well settle the arguments at the same time:

async loadingWrap (func) {
  this.loading = true;
  let result = await func();
  this.loading = false
  return result; 
}

And in some async method, note the callback function:

  let result = await this.loadingWrap(
    () => this.someAsyncFunction({ param: 'Value' })
  );
  console.log(result);

Upvotes: 0

Related Questions