Tom687
Tom687

Reputation: 312

Repeat async function until true

I have an async function that checks for the status of an order (checkOrderStatus()). I would like to repeat this function until it returns either "FILLED" or "CANCELED", then use this return value in another function to decide to continue or stop the code. Every order goes through different status before being "FILLED" or "CANCELED", therefore the need to repeat the checkOrderStatus() function (it is an API call).

What I have now is this, to repeat the checkOrderStatus() function:

const watch = filter => {
    return new Promise(callback => {
        const interval = setInterval(async () => {
            if (!(await filter())) return;
            clearInterval(interval);
            callback();
        }, 1000);
    });
};

const watchFill = (asset, orderId) => {
    return watch(async () => {
        const { status } = await checkOrderStatus(asset, orderId);

        console.log(`Order status: ${status}`);

        if (status === 'CANCELED') return false;
        return status === 'FILLED';
    });
};

I then call watchFill() from another function, where I would like to check its return value (true or false) and continue the code if true or stop it if false:

const sellOrder = async (asset, orderId) => {
    try {
        const orderIsFilled = await watchFill(asset, orderId);
        
        if (orderIsFilled) {
            //… Continue the code (status === 'FILLED'), calling other async functions …
        }
        else {
            //… Stop the code
            return false;
        }
    }
    catch (err) {
        console.error('Err sellIfFilled() :', err);
    }
};

However, this does not work. I can see the status being updated in the terminal via the console.log in watchFill(), but it never stops and most importantly, the value in the orderIsFilled variable in sellOrder() does not get updated, whatever the value returned by watchFill() becomes.

How can I achieve the desired behavior?

Upvotes: 2

Views: 2026

Answers (3)

Richie Bendall
Richie Bendall

Reputation: 9172

The watch function clears the interval timer after the first call if filter resolves with false. setInterval doesn't wait for an async function to finish executing either so you'll have to create a loop yourself. Try this:

const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));

const watch = async check => {
    while (true) {
        if (await check()) {
            return;
        }

        await delay(1000);
    }
};

Because watch only resolves when check succeeds, it is not possible to fail so you don't need to check for it (this might be a bug in your code):

const sellOrder = async (asset, orderId) => {
    try {
        await watchFill(asset, orderId);
        
        //… Continue the code (status === 'FILLED'), calling other async functions …
    }
    catch (err) {
        console.error('Err sellIfFilled() :', err);
    }
};

p-wait-for contains an excellent implementation of this. You can use it like so:

import pWaitFor from 'p-wait-for';

const watchFill = (asset, orderId) => pWaitFor(async () => {
    const { status } = await checkOrderStatus(asset, orderId);

    console.log(`Order status: ${status}`);

    if (status === 'CANCELED') return false;
    return status === 'FILLED';
}, {
    interval: 1000,
    leadingCheck: false
});

Upvotes: 1

ggorlen
ggorlen

Reputation: 56875

watch never calls resolve (in the original code, this is misleadingly named callback()) with any value, so there's no way const orderIsFilled = await watchFill(asset, orderId); will populate orderIsFilled with anything but undefined.

If you save the result of await filter() in a variable and pass it to callback as callback(result), your code seems like it should work.

That said, the code can be simplified by using a loop and writing a simple wait function. This way, you can return a value (more natural than figuring out how/when to call resolve), keep the new Promise pattern away from the logic and avoid dealing with setInterval and the bookkeeping that goes with that.

const wait = ms =>
  new Promise(resolve => setTimeout(resolve, ms))
;

const watch = async (predicate, ms) => {
  for (;; await wait(ms)) {
    const result = await predicate();
    
    if (result) {
      return result;
    }
  }
};

/* mock the API for demonstration purposes */
const checkOrderStatus = (() => {
  let calls = 0;
  return async () => ({
    status: ++calls === 3 ? "FILLED" : false
  });
})();

const watchFill = (asset, orderId) =>
  watch(async () => {
    const {status} = await checkOrderStatus();
    console.log(`Order status: ${status}`);
    return status === "CANCELLED" ? false : status === "FILLED";
  }, 1000)
;

const sellOrder = async () => {
  try {
    const orderIsFilled = await watchFill();
    console.log("orderIsFilled:", orderIsFilled);
  }
  catch (err) {
    console.error('Err sellIfFilled() :', err);
  }
};
sellOrder();

Upvotes: 1

boolfalse
boolfalse

Reputation: 2101

You can use recursive functionality like this:

const checkOrderStatus = async () => {
    // ... function does some work ...
    await someOtherFunction() // you can use here the other async function as well
    // ... function does some more work after returning from await ...
    if(/* if status is FILLED or CANCELED */) {
        // return true or false or some info about response for your needs
    } else {
        checkOrderStatus();
    }
}

// this will response back when status will be FILLED or CANCELED
await checkOrderStatus();

Upvotes: 1

Related Questions