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
Theclick-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
- Validate the filter. A listener can pass
{ code }to hear about one link only, and leave it out to hear every click. Acodethat 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.namespacematters in Chapter 7, when a browser tab binds from its own namespace.
Announce clicks from link
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: addlink::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
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 inclick-streamer/watch.ts: uncomment its Ch. 5 block (a
fully commented file runs, prints nothing and exits). The code:
click-streamer/watch.ts
click-streamer folder and leave it running:
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:
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 dedicatedclick-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.