Events
Events and subscribers provide the ability to asynchronously execute code when an event is triggered in your application.
Keel currently supports three types of events at the model level; create
, update
and delete
events. These events are fired when a model undergoes the relevant mutation; whether it be due to an action or from using the Model API (opens in a new tab) in a job, hook, custom function, or even in a subscriber.
These event types can be configured to execute a subscriber. Similar to jobs, subscribers are TypeScript functions which you provide your own implementation for. This means you can write custom code to respond to events that are triggered while your application is running.
The execution of a subscriber function (in response to an event) happens asynchronously. This means that the subscriber function will run after the execution of the action or function which triggered it.
Subscribing to events
Keel provides you the convenience of being able to subscribe to events with the @on
attribute. Take the following schema as an example.
model Member {
fields {
name Text
email Text
emailVerified Boolean @default(false)
}
actions {
create createMember() with (name, email)
update updateEmail(id) with (email) {
@set(member.emailVerified = false)
}
}
@on([create, update], verifyEmail)
}
Here we have subscribed to the create
and update
event types on the Member
model. When either of these events are fired, the verifyEmail
function will be executed.
Note that a subscriber function can respond to any number of event types, even those from a different model. Furthermore, an event can be listened to by any number of subscribers.
Events are only fired after the completion of the action or function that triggered it. For example, a job might perform many mutations, but the events will only fire after the job has completed.
Subscriber functions
Subscriber functions defined in your schema, such as with verifyEmail
in the above example, need to be implemented in a directory called ./subscribers
.
Running keel generate
with the CLI will create this file for you. The code generated for the verifyEmail
subscriber function will look like this:
import { VerifyEmail } from "@teamkeel/sdk";
export default VerifyEmail(async (ctx, event) => {
// your code here
});
Arguments
Subscriber functions receive two arguments, namely:
ctx
- a context object which contains environment variables and secretsevent
- the details of the event that occurred, including a payload of the mutated model
Continuing with our example, if a create
event type is triggered on the Member
model, the following TypeScript definition of the event
argument in the subscriber function would be generated:
export interface MemberCreatedEvent = {
// The unique name of this event, as a string literal
eventName: "member.created"
// The timestamp at which the event occured
occurredAt: DateTime,
// The identity that triggered the event, if any
identityId?: string,
// The targeted model which was mutated
target: {
// The id of the mutated model
id: string,
// The name of the model type
type: "Member",
// The model's data at the time of the event
data: Member
// The model's data before the mutation which caused this event
previousData: Member
}
}
The event.target.data
property provides the full model's data to the subscriber function. Take note of how the value of the data
property differs based on the event type:
create
- provides the newly persisted modelupdate
- provides the persisted model after it has been updateddelete
- provides the model at the time it was deleted
The event.target.previousData
property provides the full model's data before the mutation which caused the event. This is only available on update
and delete
event types.
Discerning between events
Because a subscriber function can respond to various types of events, you will likely need a way to discriminate between these event types within the code of your function.
The event
argument is a union type (opens in a new tab) and the eventName
property is a string literal type (opens in a new tab). These bits of TypeScript magic let us safely switch across the different event types.
Below we demonstrate this by providing an example implementation for the verifyEmail
subscriber function.
import { VerifyEmail } from "@teamkeel/sdk";
export default VerifyEmail(async (ctx, event) => {
switch (event.eventName) {
case "member.created":
// Welcome member and verify their email address.
await sendWelcomeMail(event.target.email);
break;
case "member.updated":
// Verify their email address if it was changed.
if (!event.target.data.emailVerified) {
await sendVerifyMail(event.target.email);
}
break;
default:
break;
}
})
Failed subscribers
An exception thrown in your subscriber function will not cause the action, job, or whatever call that triggered the event, to fail. Therefore, subscribers will fail silently. You can, however, discover failed subscriber functions by inspecting trace data in the Keel console.
Also note that a failure in one subscriber function will not stop other events from being processed.
If a subscriber function fails to execute, then the event will return to the queue and will be reprocessed again after 80 seconds. Executing a subscriber function will undergo three attempts, after which the event will be deleted.
Events will fire regardless of whether the action or function that triggered the event succeeds or fails. It all depends on what changes eventually persisted in the database. When a rollback occurs, due to some failure in a write
custom function, for example, then no events will fire because no changes would have been committed.
Event chaining
Because a subscriber function is able to mutate data using the Model API, it is therefore also able to trigger events itself. This makes it possible to create a chain of events, which is a powerful feature of events and subscribers.
However, this also makes it possible to end up with an unintentional never-ending circuit of execution of events. In order to protect you from run-away executions like this, which could be very costly, we have introduced a depth limit of 15 for event chaining.
Quotas
The following table lists the current limits and quotas for events and subscribers.
Description | Threshold |
---|---|
Maximum subscriber function execution time | 70 seconds |
Maximum number of concurrently executing events | 10 |
Number of execution attempts for a failing subscriber function | 3 attempts |
Backoff duration betweeen retry attempts | 80 seconds |
Event chain depth limit | 15 |
Maximum event message size | 256 kB |
Testing
Events could form a crucial part of your application, and so Keel has made it possible for you to test their behaviour by providing support for events and subscribers in tests.
Events in tests are synchronous. This means that you can always expect all subscriber functions to have completed executing before your action or function call returns. This makes testing events easier.
import { actions, models } from "@teamkeel/testing";
import { test, expect } from "vitest";
test("Email is verified on member creation", async () => {
// This will trigger a create event and execute the verifyEmail subscriber function
const member = await actions.createMember({ name: "Mary", "mary@keel.so" })
// Check that emailVerified was set to true by the verifyEmail subscriber function
const updated = await models.members.findOne({ id: member.id });
expect(updated.emailVerified).toBeTruthy();
});
It is also possible to simply run your subscriber function directly, without the need to trigger an event. This is a useful way to isolate and test just the function's implementation.
import { subscribers, models } from "@teamkeel/testing";
import { test, expect } from "vitest";
test("verifyEmail subscriber", async () => {
const member = await actions.createMember({ name: "Mary", "mary@keel.so" })
// The event that will be passed into the subscriber function
const event = {
eventName: "member.created" as const,
occurredAt: new Date(),
target: {
id: mary.id,
type: "Member",
data: member,
},
};
// Execute the subscriber function
await subscribers.verifyEmail(event);
// Check that emailVerified was set to true by the verifyEmail subscriber function
const updated = await models.members.findOne({ id: member.id });
expect(updated.emailVerified).toBeTruthy();
});