In the previous chapter, Task Lifecycle Orchestration, we learned how to create, update, and remove tasks from our global "whiteboard."
However, there is a missing piece. When a background script runs, it happens in a separate process (like a hidden kitchen). Our main application (the waiter) doesn't inherently know when the food is ready. We need a way to connect these two worlds.
Imagine you are a security guard responsible for a large building.
In software, Polling is that security guard. Instead of freezing our application to wait for a command to finish (which freezes the UI), we set up a "heartbeat" that quickly checks the status of all tasks every second.
"The User runs a long installation script (e.g., npm install)."
This might take 2 minutes. We want to show a progress bar and log messages in real-time without the application freezing up.
We don't expect you to write the polling engine yourselfβthe framework handles it. However, understanding the flow is crucial for debugging why a task might not be updating.
pollTasks().running.Let's visualize the "Security Guard" (The Poller) interacting with the system.
The magic happens in framework.ts. Let's break down the actual code into bite-sized pieces to see how this is orchestrated.
We define how often the guard walks the rounds.
// Standard polling interval for all tasks
export const POLL_INTERVAL_MS = 1000
Explanation: This constant defines the rhythm. 1000ms (1 second) is a balance between responsiveness (UI updates fast) and performance (not wasting CPU).
This function is called every time the timer ticks.
export async function pollTasks(
getAppState: () => AppState,
setAppState: SetAppState,
): Promise<void> {
const state = getAppState()
// 1. Calculate what changed (Deltas)
const { attachments, updatedTaskOffsets, evictedTaskIds } =
await generateTaskAttachments(state)
// 2. Apply changes to the global state
applyTaskOffsetsAndEvictions(setAppState, updatedTaskOffsets, evictedTaskIds)
}
Explanation: pollTasks is the coordinator. It asks "What changed?" (generateTaskAttachments) and then applies those changes (applyTaskOffsetsAndEvictions).
Inside generateTaskAttachments, we look for new content. We use an Offset to remember where we stopped reading last time.
// Inside generateTaskAttachments loop...
if (taskState.status === 'running') {
// Check disk for new data starting from our last known position (offset)
const delta = await getTaskOutputDelta(
taskState.id,
taskState.outputOffset,
)
// If we found new text, record the new position
if (delta.content) {
updatedTaskOffsets[taskState.id] = delta.newOffset
}
}
Explanation: If we read 100 bytes last time, outputOffset is 100. Next time, we ask the disk: "Give me everything after byte 100." This is extremely efficient.
Finally, we update the application state so the UI can render the new text.
export function applyTaskOffsetsAndEvictions(
setAppState: SetAppState,
updatedTaskOffsets: Record<string, number>,
evictedTaskIds: string[],
): void {
setAppState(prev => {
const newTasks = { ...prev.tasks }
// Update the offset for tasks that had new output
for (const id of Object.keys(updatedTaskOffsets)) {
if (newTasks[id]) {
newTasks[id].outputOffset = updatedTaskOffsets[id]!
}
}
return { ...prev, tasks: newTasks }
})
}
Explanation: We take the newOffset we calculated and save it into the global state. The next time the poller runs, it will start reading from this new position.
You might wonder: Why read from a file? Why not just pipe the data directly from the process to the variable?
Using files as the "middleman" provides Durability. If the application crashes and restarts, the file is still on the disk. The application can read the file and "remember" exactly what happened before the crash.
In this chapter, we learned:
But waitβwe keep talking about reading from "Output Files." How does the data get into those files in the first place? And what happens if the file gets too big?
In the next chapter, we will explore exactly how we handle data flow.
Next Chapter: Hybrid Output Management
Generated by Code IQ