1

Multiple callers can subscribe to a function in progress if it returns a CommonJS promise:

let curWaitTask;
let lastWaitTime = new Date(1970, 0, 1);
let func1 = function() {
  if(curWaitTask) {
    return curWaitTask;
  }
  if((new Date() - lastWaitTime) > TWENTY_MINUTES) {
    lastWaitTime = new Date();
    curWaitTask = func2().then(func3).then(func4);
    return curWaitTask;
  }
}

Above is an expiring cache approach. The first call to func1 that happens after the twenty minute cache expires will cause the service to perform another async operation to update the cache. In the meantime, other calls to func1 may occur, which will await the same cache update operation instead of it happening again and again until the cache gets set (multiple times in a row, due to the all the calls).

Is there some way to get this behavior while using the clarity offered by the Babel await keyword, instead of a string of unwieldy .then()'s? In practice, it is getting ugly.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Brandon Arnold
  • 410
  • 1
  • 5
  • 18
  • I'm not sure I understand the question. Can't you just write `result = await operation()`? – Casey Oct 20 '16 at 00:53
  • @Casey: Say ```operation()``` takes a long time, like 1 minute. If the ```await operation()``` line is called 10 times in a 5 second period, it will execute the whole 1 minute call 10 times. But in the ```func1``` example I gave, the long ```func2.then(func3).then(func4)``` chain only happens the first time, and the following 9 calls to ```func1()``` will wait for the result of the first promise. See the difference? – Brandon Arnold Oct 20 '16 at 01:04
  • I see. But can't you just issue `curwaittask = await func1()` and then loop? – Casey Oct 20 '16 at 02:09
  • Is `lastWaitTime` ever updated? And what should a call to `func1` do when there is neither a cached value nor timeout expired? – Bergi Oct 20 '16 at 03:00
  • Surely you meant `func2()` not `func2`? – Bergi Oct 20 '16 at 03:00
  • @Bergi: Thx, good catch on ```func2()```. The set of ```lastWaitTime``` was assumed to happen within ```func4``` but I set it above for clarity. – Brandon Arnold Oct 20 '16 at 03:06
  • @BrandonArnold Thanks, I updated my answer. Setting `lastWaitTime` in `func4` vs before the `then` chain makes some timing difference but doesn't change the answer. Regardless, you still didn't answer the question what should happen when there is no `curWaitTask` but the `lastWaitTime` has not yet expired? – Bergi Oct 20 '16 at 03:12
  • @BrandonArnold Also, is `curWaitTask` ever unset or does the value stay there forever? – Bergi Oct 20 '16 at 03:13
  • @Bergi: Thanks, I think I like your answer below, reviewing it now. It is assumed the condition never occurs that ```curWaitTask``` is null except on the first iteration. Conceivably the ```.then``` chain may throw or return null but I don't think it makes the problem any clearer to account for those errors. Just illustrating the method. – Brandon Arnold Oct 20 '16 at 03:21
  • I do see the point of your question now. I'm typing on my phone, will have to review the code segment I copied this from to see what I got out of order. – Brandon Arnold Oct 20 '16 at 03:23

2 Answers2

2

Just wrap the "unwieldy then chain" in an async function and use await in there:

let curWaitTask;
let lastWaitTime = new Date(1970, 0, 1);
function func1() {
  if (!curWaitTask && (new Date() - lastWaitTime) > TWENTY_MINUTES) {
    lastWaitTime = new Date();
    curWaitTask = doTask();
  }
  return curWaitTask;
}
async function doTask() {
  return func4(await func3(await func2()));
}

It is (fortunately?) not possible inside an async function to refer to the new promise that is created from the current invocation, so using async function func1 is not really sensible here.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
2

You asked if multiple callers could subscribe to the same async function, so let's see if we can solve this within func1 and keep it async (with help from Bergi!):

let curTask;
let lastTime = new Date(1970, 0, 1);

async function func1() {
  if (curTask && (new Date() - lastTime) < 10000) {
    return await curTask;
  }
  return await (curTask = (async () => {
    lastTime = new Date();
    return await func4(await func3(await func2()));
  })());
}

We can await promises, not just async functions.

We put the meat of the work in a second async function that we don't immediately await, thereby getting its promise instead, which we initialize curTask with synchronously.

jib
  • 40,579
  • 17
  • 100
  • 158
  • That usage of `new Promise` and `resolve`/`reject` is quite questionable and highly error-prone. I'd rather recommend to use an `async` immediately-invoked function expression if you really needed something like this. – Bergi Oct 20 '16 at 23:17
  • @Bergi Questionable no doubt, but error-prone? It pretty much mimics a promise constructor pattern inside an async function, except I'm not passing an actual async function to a promise constructor... Maybe I should try that... – jib Oct 21 '16 at 00:21
  • 1
    Error-prone for three reasons: you might forget the `try`/`catch` (especially [inside `new Promise`](http://stackoverflow.com/q/23803743/1048572)) and not `reject` when appropriate, you might forget to `resolve` the promise (but try to `return` instead), or you might accidentally try to `await curTask` before calling `resolve`. Therefore, a rule of thumb: don't use `async function`s inside the promise constructor. Just do `curTask = (async () => { …; return …; }())`. – Bergi Oct 21 '16 at 06:15
  • @Bergi You're right that does basically accomplish the same thing, and simpler. Sweet, thanks! – jib Oct 21 '16 at 06:42