Skip to main content
Real-time delivery means pushing data to whoever is listening the moment it changes, like a live click counter on a dashboard. On iii that is what observing a function does: worker.observe(a, b) runs b each time a runs. A live click feed is a function that runs once per click, and anyone who wants to hear about clicks observes it, the same way you observed http::on-request and queue::on-message in earlier chapters. You’ll give the feed to a dedicated click-streamer worker so the link worker stays focused on links. A notification is not storage. The clicks already live in the clicks table from Chapter 3, so a listener that joins late reads the current total with a query, then counts the notifications that follow.
Earlier versions of this chapter used iii-stream and pubsub. If your project still has them, see Migrate from iii-stream and pubsub.

Add the worker

The click-streamer worker owns the live click feed. Uncomment the Ch. 5 block under containers:
worker-compose.yaml

Host the click feed in click-streamer

The feed is click-streamer::on-click, a function that runs once per recorded click and returns that click. What a function returns is what its observers receive, so every listener gets { id, code, clicked_at } for each click. click-streamer runs the feed each time link::record_click runs, by observing it. Create click-streamer/src/index.ts:
click-streamer/src/index.ts
Each part of that file has one job:
  • The feed returns the click. click-streamer::on-click builds the click from the three fields a listener needs and returns it. Its observers receive that value.
  • Only click-streamer calls the feed. private: true makes the engine accept calls of click-streamer::on-click from its own worker only, so no other worker can announce a click that never happened. Other workers can still observe it, which matters in Chapter 7, when a browser tab observes it.
  • The listeners are capped. onObserverAdded runs before iii accepts a new observer of the feed, and overrides iii’s default of accepting every one: past 256 listeners it throws, and the observer is rejected.
  • announce connects the feed to link. It observes link::record_click with capture: ["output"], so it runs after each successful call of link::record_click and receives what that call returned as the payload’s output. It then calls the feed with the click.
click-streamer::announce receives what link::record_click returns, so have it return the click it recorded. database::execute returns the new row’s id as last_insert_id; it travels with the click so listeners can tell a new click from one they have already counted. Replace link::record_click in link/src/index.ts:
link/src/index.ts
The link worker stays focused on links: it records the click and returns it, and click-streamer reacts to that on its own.
A live counter is best effort. iii hands each click to the listeners without waiting for them, so the click write never waits on a slow listener, and a click a listener misses is not replayed. The listener’s next read of the total corrects it.

Read the current total

A listener that connects now has missed every earlier click, and notifications are never replayed. Give it a query to start from: add link::click_summary to link/src/index.ts. It returns the number of clicks and the highest click id, so a listener knows which notifications are already included in the total.
link/src/index.ts
Restart Compose so it picks up the change:
The browser you build in Chapter 7 observes click-streamer::on-click and counts clicks live.

See it work

A listener is any worker that observes click-streamer::on-click. The scaffold ships a small one that prints each click, commented out in click-streamer/watch.ts: uncomment its Ch. 5 block (a fully commented file runs, prints nothing and exits). The code:
click-streamer/watch.ts
ready() resolves once the observer is active, so every click after that line reaches the watcher. Run it from the click-streamer folder and leave it running:
In a second terminal, create a few links and follow each:
Running the loop again is fine: the links already exist, so POST /links answers 409 with {"error":"code `stream-1` is already taken"}, and each redirect still records a new click. Change the numbers in seq to create new links. Each redirect shows up in the watcher as soon as its click row is written:
A listener that wants one link’s clicks adds a filter, and iii runs its target only for the clicks that match:
The total is always one query away:
Stop the watcher with Ctrl+C. Its observer is suspended when the watcher disconnects, and iii removes it once the grace period (30 seconds by default) ends without a reconnect.

Conclusion

Linkly now pushes every click to its listeners in real time: click-streamer observes link::record_click and runs its click-streamer::on-click feed once per click, and every listener observes the feed. The clicks table stays the source of truth behind link::click_summary. Next, in Ch. 6: Move bulk data with channels, you bulk-load links from a CSV in a single streamed upload.