Topics
Topics and Events provide a scalable, decoupled, way to communicate between functions and containers.
Topics
A topic is a named target where events can be published. They can be thought of as a subject that your functions can discuss with each other.
They're awesome for allowing serverless functions to communicate in a stateless, scalable and highly decoupled way.
Events
Events are the messages that can be published to a topic. They can be thought of as kind of notification that is sent to say that something new has happened.
Subscriptions
A subscription is something listening to a topic. You can think of it as a channel that notifies your application when something new arrives on the topic.
The basics
Creating a Topic
Before events can be published or subscribers created a topic must be defined.
import { topic } from '@nitric/sdk';const userCreatedTopic = topic('user-created').for('publishing');
Publishing an event
To send an event to a topic and notify all subscribers, use the publish()
method on the topic reference.
const data = userCreatedTopic.publish({payload: {email: 'new.user@example.com',},});
Subscribing to a topic
To execute a function when new events are published you can setup subscribers. The delay between publishing an event and a subscriber being executed is usually only a few milliseconds. This makes subscribers perfect for responding to events as they happen.
userCreatedTopic.subscribe(async (ctx) => {// Extract data from the event payload for processingconst { email } = ctx.req.json();sendWelcomeEmail(email);});
Limitation on Publishing and Subscribing
Nitric won't allow you to request publishing access and setup a subscriber in the same function.
// this isn't validimport { topic } from '@nitric/sdk';const loopTopic = topic('infinite').for('publishing');loopTopic.subscribe(async (ctx) => {await loopTopic.publish({ payload: {} });});
The limitation exists to protect you from infinite loops in deployed functions where a function calls itself indirectly via a topic. These sorts of mistakes can lead to large unintentional cloud charges - something you probably want to avoid.
Reliable event handling
If a subscriber encounters an error or is terminated before it finishes processing an event, what happens? Is the event lost?
Nitric deploys topics to cloud services that support "at-least-once delivery". Events are usually delivered exactly once, in the same order that they're published. However, to prevent lost events, they're sometimes delivered more than once or out of order.
Typically, retries occur when a subscriber doesn't respond successfully, like when unhandled exceptions occur. You'll want to make sure events aren't processed again by accident or partially processed, leaving your system in an unexpected state.
Luckily, building atomic publishers and idempotent subscribers is enough to solve for this.
Atomic publishers
Basically, your publishers needs to update your database and publish associated events. If a database update fails, the events should never be sent. If the database update succeeds, the events should always publish. The two shouldn't occur independently (i.e. one shouldn't fail while the other succeeds).
One solution to this problem is the Transactional Outbox Pattern.
Idempotent subscribers
Events can be delivered more than once, but they should only be processed once. To do this your subscribers need to identify and disregard duplicate events.
Usually checking for duplicate payloads or IDs is enough. When you receive an event you've seen before don't process it, skip straight to returning a success response from your subscriber.
import { topic } from '@nitric/sdk';import { isDuplicate } from '../common';const updates = topic('updates');updates.subscribe((ctx) => {if (isDuplicate(ctx.req)) {return ctx;}// not a duplicate, process the event//...});
If you're checking for duplicate IDs, make sure publishers can't resend failed events with new IDs
What's next?
- Learn more about topics in our reference docs.