Why your alert did not fire
Your rules are documents and your evaluator is tested. The alert still will not work, and this lesson is about being honest regarding why.
Four things an alert needs
- Something that runs on a schedule. Not a page, which only runs when open.
- Data fresh enough that the alert is not already history when it arrives.
- Somewhere to keep state, so it does not tell you the same thing twice.
- A way to reach you when you are not looking at the tab.
Your cockpit has exactly one of these. Unit 1 gave you number 3 by moving persistence to the server. The other three are the subject of the rest of this course.
The browser cannot do this, and the reasons are worth knowing
It is not a limitation you can engineer around with a better setInterval:
- A closed tab runs nothing you can schedule. No timer, and no worker you can wake on your own — a service worker only stirs when something outside pushes to it, and that something is a server you do not have.
- A background tab is throttled. Browsers clamp timers in hidden tabs to about once a minute or less, so your five-second poll silently becomes something else.
- A sleeping laptop runs nothing, and wakes up with a timer that thinks no time passed.
- Two open tabs both evaluate, so you get two notifications and both write
lastFiredAt.
The last one is the giveaway that this belongs somewhere else: correctness now depends on how many tabs you happen to have open.
Say it in the interface
One line, near the rules: "Rules are checked while this page is open."
This is the same honesty as course 1, and it matters more now, because a cockpit looks like infrastructure. It has panels and alerts and a refresh indicator, and a person will reasonably assume it is watching for them. The worst outcome of this entire track is someone trusting an alert that was never going to fire.
What the real version costs, in calls
Price it before you build it, with the table from course 1.
A server checking 12 symbols every 5 minutes during a 6.5-hour session: 78 checks × 12 symbols = 936 calls a day, since /real-time is billed per symbol. Every minute instead: 390 × 12 = 4,680.
Against a free allowance of 20 calls a day, neither is close. That gap is not a bug in your design — it is the wall, arriving with a number attached, and unit 4 is where you meet it properly.
Build the evaluator so it can move
Even though you are not deploying a scheduler in this course, write the evaluation loop so it does not care where it runs:
async function checkAll(rules: Rule[], fetchQuotes: Fetcher, now: number): Promise<Fired[]>
Time and fetching are parameters, not ambient. That makes it testable today with fixtures and a fake clock, and deployable tomorrow to a cron job without a rewrite.
This is course 3's discipline again: a function whose inputs you control is a function you can test, and one that reads the clock directly is neither testable nor portable.
Try it now
Do the arithmetic for your own rules — symbols × checks per day — and write it in PLAN.md next to your allowance. That single line is the most useful thing in this unit, and it is the number the last lesson of the course comes back to.