Build a feature-toggle client that polls for updates with start/stop control
Implement a TogglesClient that keeps a map of feature flags fresh by polling an endpoint. Constructor takes (initialData, { url, interval }). Provide getToggle(key) (reads the current value), start()/stop() (begin/halt polling), and forceUpdate() (refetch immediately). Requirement: never let two refreshes overlap, even if a fetch is slow.
class TogglesClient {
constructor(initialData, { url, interval }) {
// your code here
}
}
Write the implementation.
Schedule with setTimeout, not setInterval: in the callback await the fetch, then reschedule in a .finally, so a slow request delays the next poll instead of overlapping it. forceUpdate refreshes now; getToggle reads data[key]; stop calls clearTimeout; start re-arms the timer. Self-rescheduling only after completion is what prevents concurrent in-flight refreshes.
- ✗Using
setInterval, which can stack overlapping fetches when a request is slower than the interval - ✗Rescheduling before the fetch resolves instead of in
.finally, reintroducing overlap - ✗Forgetting to
clearTimeoutinstop(), leaving a poll still pending
- →Why does self-rescheduling
setTimeoutavoid overlapping requests thatsetIntervalallows? - →Why reschedule in
.finallyrather than in the success handler?
Solution
A self-rescheduling setTimeout in .finally guarantees no overlap.
class TogglesClient {
#data;
#url;
#interval;
#timer = null;
constructor(initialData, { url, interval }) {
this.#data = initialData;
this.#url = url;
this.#interval = interval;
this.start();
}
#schedule() {
this.#timer = setTimeout(() => {
this.forceUpdate().finally(() => this.#schedule());
}, this.#interval);
}
async forceUpdate() {
const res = await fetch(this.#url);
this.#data = await res.json();
}
getToggle(key) {
return this.#data[key];
}
start() {
if (!this.#timer) this.#schedule();
}
stop() {
clearTimeout(this.#timer);
this.#timer = null;
}
}
How it works
Polling is built on a setTimeout that re-arms itself in .finally — that is, only after a refresh has settled (succeeded or failed). So the next request starts no sooner than the previous one finished: two refreshes never run at the same time, even on a slow network.
setInterval cannot do this — it fires on the clock regardless of whether the prior request finished, and a slow fetch causes stacking. getToggle reads the already-cached map, forceUpdate refreshes immediately, and stop/start clear and arm the timer. </content>