An async countdown latch is a one-shot gate that stays closed until a fixed number of events have happened, then releases every waiter at once. You tell it up front how many events to wait for; each event calls countDown(), and anyone who called wait() resumes the moment the last event reports in. It is the async cousin of a countdown latch from concurrent programming (for example Java's CountDownLatch) — the tool for "run this once all N of those have finished."
Implement asyncBarrierLatch(count). It returns three functions: countDown() records one event, wait() returns a promise that resolves when the latch opens, and remaining() reports how many events are still needed.
type Latch = {
countDown(): void; // record one event; the count-th call opens the latch
wait(): Promise<void>; // resolves when the latch opens (now, if already open)
remaining(): number; // events still needed before the latch opens
};
function asyncBarrierLatch(count: number): Latch;
const latch = asyncBarrierLatch(3);
latch.wait().then(() => console.log('all ready'));
latch.remaining(); // 3
latch.countDown(); // remaining 2
latch.countDown(); // remaining 1
latch.countDown(); // remaining 0 -> logs "all ready"
const latch = asyncBarrierLatch(2);
const a = latch.wait();
const b = latch.wait();
latch.countDown();
latch.countDown(); // a and b both resolve together
latch.wait(); // already open -> resolves immediately
latch.countDown(); // no-op: nothing left to count
latch.remaining(); // 0
countDown() has run exactly count times, not on the first one.wait() promises can be outstanding at once; they all resolve at the single moment the latch opens.wait() after the latch is open returns an already-resolved promise. The latch never closes again.remaining() reaches zero, further countDown() calls do nothing: the count never goes negative and waiters are never released twice.count is an integer of at least 1. Each wait() resolves with undefined.countDown() drives every resolution. You never need setTimeout.You'll build a one-shot gate that stays shut until a set number of things have finished, then lets every waiter through at once.
You kick off three downloads and want to run a final step only after all three finish. You don't care about their order or what they return — only that every one of them is done. A countdown latch tracks exactly that: you tell it how many things to wait for, each finished thing calls countDown(), and anyone who called wait() resumes the instant the last one reports in.
Picture a gate held shut by a counter set to count. Every countDown() turns the dial down by one. While the dial is above zero the gate stays shut and everyone who called wait() stands behind it. The instant the dial reaches zero the gate swings open and the whole crowd walks through together — and it never shuts again, so anyone who arrives later walks straight through.
The obvious version just flips a flag the first time something reports in:
function asyncBarrierLatchNaive(count) {
let open = false; // a plain flag — the count is ignored
const waiters = [];
function countDown() {
open = true; // opens on the very first call
waiters.forEach((resolve) => resolve());
waiters.length = 0;
}
function wait() {
if (open) return Promise.resolve();
return new Promise((resolve) => waiters.push(resolve));
}
function remaining() {
return open ? 0 : count; // jumps straight from count to 0
}
return { countDown, wait, remaining };
}
This throws away the one number the problem is about. With a count of 3, the first countDown() flips open to true and releases everyone — but two of your three downloads are still running, so the final step fires too early. And remaining() never shows 2 or 1; it leaps from 3 to 0. A latch has to count the calls, not just remember that one happened.
function asyncBarrierLatch(count) {
let remainingCount = count; // countDown() calls still needed before the gate opens
const waiters = []; // resolve fns of wait() promises made while still closed
function countDown() {
if (remainingCount <= 0) return; // already open — extra calls do nothing
remainingCount--;
if (remainingCount === 0) {
// The latch just opened. Release every parked waiter together.
const pending = waiters.splice(0); // read and clear the queue in one step
for (const resolve of pending) resolve();
}
}
function wait() {
// Already open -> resolve now. Otherwise park until countDown opens the gate.
if (remainingCount <= 0) return Promise.resolve();
return new Promise((resolve) => {
waiters.push(resolve);
});
}
function remaining() {
return remainingCount; // never negative: countDown stops decrementing at zero
}
return { countDown, wait, remaining };
}
module.exports = { asyncBarrierLatch };
The single boolean becomes a counter, remainingCount, and that changes everything. countDown() opens the gate only on the call that drives the counter to exactly zero; the remainingCount <= 0 guard at the top makes every later call a no-op, so the count never drifts below zero and waiters are never released twice. And because a wait() made after the gate is open sees remainingCount <= 0, it returns an already-resolved promise instead of parking a resolver that would never be called. remainingCount and waiters live in the closure — the private scope the returned functions share, see MDN on closures — so all three functions read and write the same counter and the same queue.
Start with asyncBarrierLatch(3): remainingCount is 3 and waiters is empty. Two callers, X and Y, each call wait(). The counter is above zero, so each gets a fresh pending promise and its resolve is pushed onto waiters — the queue now holds [resolveX, resolveY]. Now the countdowns arrive:
countDown() — remainingCount drops to 2. Not zero, so nothing is released.countDown() — remainingCount drops to 1. Still shut.countDown() — remainingCount drops to 0. This is the opening call: we splice the two resolvers out of waiters and run them, so X's and Y's promises both resolve with undefined on the same tick.countDown() — the guard sees the counter is already 0 and returns at once. The count stays 0; nobody is re-fired.If a third caller, Z, calls wait() now, it sees the counter is 0 and gets an already-resolved promise — no waiting.
count and releases everyone after one call. Track a counter and open only when it reaches zero.remainingCount <= 0 guard, surplus countDown() calls push the counter to -1, -2, and remaining() starts reporting nonsense. Guard at the top so extra calls are no-ops.remainingCount <= 0 instead of only on the call that reaches zero, later countDown() calls re-run the loop. Fire once, on the exact transition to zero.wait() — if wait() always parks a resolver, a call made after the gate opened waits forever, because no future countDown() will drain the queue. Check remainingCount <= 0 first and return Promise.resolve().splice(0) reads the pending resolvers and empties the queue in one step.await sugar — since wait() returns a promise, a caller can await latch.wait() inside an async function to pause until the group is ready.wait() against a timeout with Promise.race so a caller can give up if the latch takes too long to open.countDown() return the new remaining count so callers can log progress without a separate remaining() call.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
An async countdown latch is a one-shot gate that stays closed until a fixed number of events have happened, then releases every waiter at once. You tell it up front how many events to wait for; each event calls countDown(), and anyone who called wait() resumes the moment the last event reports in. It is the async cousin of a countdown latch from concurrent programming (for example Java's CountDownLatch) — the tool for "run this once all N of those have finished."
Implement asyncBarrierLatch(count). It returns three functions: countDown() records one event, wait() returns a promise that resolves when the latch opens, and remaining() reports how many events are still needed.
type Latch = {
countDown(): void; // record one event; the count-th call opens the latch
wait(): Promise<void>; // resolves when the latch opens (now, if already open)
remaining(): number; // events still needed before the latch opens
};
function asyncBarrierLatch(count: number): Latch;
const latch = asyncBarrierLatch(3);
latch.wait().then(() => console.log('all ready'));
latch.remaining(); // 3
latch.countDown(); // remaining 2
latch.countDown(); // remaining 1
latch.countDown(); // remaining 0 -> logs "all ready"
const latch = asyncBarrierLatch(2);
const a = latch.wait();
const b = latch.wait();
latch.countDown();
latch.countDown(); // a and b both resolve together
latch.wait(); // already open -> resolves immediately
latch.countDown(); // no-op: nothing left to count
latch.remaining(); // 0
countDown() has run exactly count times, not on the first one.wait() promises can be outstanding at once; they all resolve at the single moment the latch opens.wait() after the latch is open returns an already-resolved promise. The latch never closes again.remaining() reaches zero, further countDown() calls do nothing: the count never goes negative and waiters are never released twice.count is an integer of at least 1. Each wait() resolves with undefined.countDown() drives every resolution. You never need setTimeout.You'll build a one-shot gate that stays shut until a set number of things have finished, then lets every waiter through at once.
You kick off three downloads and want to run a final step only after all three finish. You don't care about their order or what they return — only that every one of them is done. A countdown latch tracks exactly that: you tell it how many things to wait for, each finished thing calls countDown(), and anyone who called wait() resumes the instant the last one reports in.
Picture a gate held shut by a counter set to count. Every countDown() turns the dial down by one. While the dial is above zero the gate stays shut and everyone who called wait() stands behind it. The instant the dial reaches zero the gate swings open and the whole crowd walks through together — and it never shuts again, so anyone who arrives later walks straight through.
The obvious version just flips a flag the first time something reports in:
function asyncBarrierLatchNaive(count) {
let open = false; // a plain flag — the count is ignored
const waiters = [];
function countDown() {
open = true; // opens on the very first call
waiters.forEach((resolve) => resolve());
waiters.length = 0;
}
function wait() {
if (open) return Promise.resolve();
return new Promise((resolve) => waiters.push(resolve));
}
function remaining() {
return open ? 0 : count; // jumps straight from count to 0
}
return { countDown, wait, remaining };
}
This throws away the one number the problem is about. With a count of 3, the first countDown() flips open to true and releases everyone — but two of your three downloads are still running, so the final step fires too early. And remaining() never shows 2 or 1; it leaps from 3 to 0. A latch has to count the calls, not just remember that one happened.
function asyncBarrierLatch(count) {
let remainingCount = count; // countDown() calls still needed before the gate opens
const waiters = []; // resolve fns of wait() promises made while still closed
function countDown() {
if (remainingCount <= 0) return; // already open — extra calls do nothing
remainingCount--;
if (remainingCount === 0) {
// The latch just opened. Release every parked waiter together.
const pending = waiters.splice(0); // read and clear the queue in one step
for (const resolve of pending) resolve();
}
}
function wait() {
// Already open -> resolve now. Otherwise park until countDown opens the gate.
if (remainingCount <= 0) return Promise.resolve();
return new Promise((resolve) => {
waiters.push(resolve);
});
}
function remaining() {
return remainingCount; // never negative: countDown stops decrementing at zero
}
return { countDown, wait, remaining };
}
module.exports = { asyncBarrierLatch };
The single boolean becomes a counter, remainingCount, and that changes everything. countDown() opens the gate only on the call that drives the counter to exactly zero; the remainingCount <= 0 guard at the top makes every later call a no-op, so the count never drifts below zero and waiters are never released twice. And because a wait() made after the gate is open sees remainingCount <= 0, it returns an already-resolved promise instead of parking a resolver that would never be called. remainingCount and waiters live in the closure — the private scope the returned functions share, see MDN on closures — so all three functions read and write the same counter and the same queue.
Start with asyncBarrierLatch(3): remainingCount is 3 and waiters is empty. Two callers, X and Y, each call wait(). The counter is above zero, so each gets a fresh pending promise and its resolve is pushed onto waiters — the queue now holds [resolveX, resolveY]. Now the countdowns arrive:
countDown() — remainingCount drops to 2. Not zero, so nothing is released.countDown() — remainingCount drops to 1. Still shut.countDown() — remainingCount drops to 0. This is the opening call: we splice the two resolvers out of waiters and run them, so X's and Y's promises both resolve with undefined on the same tick.countDown() — the guard sees the counter is already 0 and returns at once. The count stays 0; nobody is re-fired.If a third caller, Z, calls wait() now, it sees the counter is 0 and gets an already-resolved promise — no waiting.
count and releases everyone after one call. Track a counter and open only when it reaches zero.remainingCount <= 0 guard, surplus countDown() calls push the counter to -1, -2, and remaining() starts reporting nonsense. Guard at the top so extra calls are no-ops.remainingCount <= 0 instead of only on the call that reaches zero, later countDown() calls re-run the loop. Fire once, on the exact transition to zero.wait() — if wait() always parks a resolver, a call made after the gate opened waits forever, because no future countDown() will drain the queue. Check remainingCount <= 0 first and return Promise.resolve().splice(0) reads the pending resolvers and empties the queue in one step.await sugar — since wait() returns a promise, a caller can await latch.wait() inside an async function to pause until the group is ready.wait() against a timeout with Promise.race so a caller can give up if the latch takes too long to open.countDown() return the new remaining count so callers can log progress without a separate remaining() call.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.