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 there is no special channel for this: a worker declares its own trigger type, and anyone who wants to hear about clicks binds a function to it, the same way you bound functions to http and durable:subscriber in earlier chapters. You’ll give that job 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

Own a trigger type in click-streamer

A trigger type has a provider: the worker that registered it. When another worker binds a function to the type, the engine calls the provider’s registerTrigger with that binding, and calls unregisterTrigger when the binding goes away or its worker disconnects. The provider keeps its own table of bindings and invokes their functions when something happens. Create click-streamer/src/index.ts:
click-streamer/src/index.ts
A few things in there are the provider’s job, because nobody else can do them:
  • Validate the filter. A listener can pass { code } to hear about one link only, and leave it out to hear every click. A code that is present always filters, even "". Anything that is not a string is rejected at bind time instead of failing later.
  • Bound the fan-out. The table has a cap, and each delivery uses TriggerAction.Void(), so a slow listener never holds up the others.
  • Deliver to the listener’s namespace. Passing binding.namespace matters in Chapter 7, when a browser tab binds from its own namespace.
Now have link::record_click hand each click to click-streamer::broadcast, after the row is written. 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
In the new code above we didn’t use await and set the action to TriggerAction.Void(). This causes the function to return immediately before the broadcast completes. A live counter is best effort: if a notification is lost, the listener’s next read of the total corrects it, so the click write never waits on the listeners.

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 binds to click-streamer::click and counts clicks live.

See it work

A listener is any worker that binds a function to the trigger type. 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
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:
The total is always one query away:
Stop the watcher with Ctrl+C. Its binding goes away with it, and click-streamer drops it from its table.

Conclusion

Linkly now pushes every click to its listeners in real time through a dedicated click-streamer worker that owns the click-streamer::click trigger type, while 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.