Welcome to the fourth chapter of the release-notes tutorial!
In the previous chapter, Asynchronous Command Handler, we learned how to write the code that fetches data from the internet. We learned that fetching data takes time, and we used async/await to handle that waiting period.
However, there is a problem. What if the user has a slow internet connection? If fetching the data takes 5 seconds, the user stares at a blank screen for 5 seconds. In the world of Command Line Interfaces (CLIs), 5 seconds feels like an eternity.
In this chapter, we will solve this using an Optimistic Fetching Strategy.
To understand this strategy, let's look at a real-world analogy. You want to know what is happening in the world.
The Optimistic Strategy works like this: You try to turn on the TV. You give it exactly 500 milliseconds to connect.
We prioritize speed over freshness. We prefer showing slightly older data instantly rather than making the user wait for new data.
In JavaScript, we implement this time limit using a concept called a Race.
Imagine a race track with two runners:
We start both runners at the same time. The rule is simple: We only listen to the winner.
First, we need to create the "Timer" runner. This is a piece of code that intentionally fails (rejects) after a set time.
// Define a timeout limit (500 milliseconds)
const timeoutPromise = new Promise<void>((_, reject) => {
// Set a timer. When time is up, trigger an error.
setTimeout(() => {
reject(new Error('Timeout'))
}, 500)
})
Explanation:
setTimeout: A built-in JavaScript function that waits for a specific amount of time.reject: This tells the program, "Something went wrong" (in this case, we ran out of time).
Now we use Promise.race(). This function takes a list of promises (tasks) and returns the result of the first one to finish.
// We need two contestants
const networkTask = fetchAndStoreChangelog() // Tries to get data
const timerTask = timeoutPromise // Fails after 500ms
try {
// Start the race!
await Promise.race([networkTask, timerTask])
// If we reach this line, the Network won!
// We can confidently use fresh data.
} catch (error) {
// If we land here, the Timer won (or the network failed).
// We should switch to the backup plan.
}
Explanation:
Promise.race([...]): Starts both tasks simultaneously.networkTask finishes in 100ms, the race is over, and we continue inside the try block.networkTask takes 2000ms, timerTask will trigger the error at 500ms. The code jumps immediately to the catch block.
Now let's look at how we combine this race with our "Newspaper" (Cache) fallback in our release-notes.ts file.
We try to get the fresh notes, but we wrap it in our race logic.
// --- File: release-notes.ts ---
// 1. Prepare to hold our notes
let freshNotes = []
try {
// 2. Define the strict 500ms time limit
const timeout = new Promise((_, rej) => setTimeout(rej, 500))
// 3. Race the network against the clock
await Promise.race([fetchAndStoreChangelog(), timeout])
// 4. If successful, load the new data
freshNotes = await getStoredChangelog()
} catch {
// 5. If it times out, do nothing here. We handle it below.
}
After the race is over, we check what we have. Did we get fresh notes? If not, do we have old notes?
// --- File: release-notes.ts (continued) ---
// Scenario A: The Network was fast. Show fresh notes.
if (freshNotes.length > 0) {
return { type: 'text', value: formatReleaseNotes(freshNotes) }
}
// Scenario B: Network was slow. Check the Cache (The Newspaper).
const cachedNotes = await getStoredChangelog()
if (cachedNotes.length > 0) {
return { type: 'text', value: formatReleaseNotes(cachedNotes) }
}
Explanation:
freshNotes.freshNotes is empty (because of a timeout or error), we look at cachedNotes.What happens internally when the CLI executes this strategy? Let's visualize the "Slow Network" scenario, which is the most interesting one.
It is important to understand that JavaScript cannot "cancel" the network request easily.
When the Timer wins the race:
await Promise.race line finishes and throws an error (which we catch).fetchAndStoreChangelog is actually still running in the background!This means that while the user sees old data now, the next time they run the command, they will see the data we just downloaded in the background. This is often called "Stale-While-Revalidate" behavior.
In this chapter, we implemented an Optimistic Fetching Strategy.
Promise.race to set a strict time limit (500ms) on our network requests.Now we have our data, whether it came from the live network or the cache. It is currently just a raw list of strings. We need to make it look professional and readable for the user.
Let's learn how to style our output in the final chapter: Response Formatting.
Generated by Code IQ