Reputation: 4343
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
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:
this.someAsyncFunction({ param: 'Value'})
this.loadingWrap(x)
where x
is the return value of step 1This 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.
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()
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
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
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
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
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