Asynchronous handler mode
async-mode
allows you to run asynchronous actions in your event handlers that need to be completed in your indexing process. Example use cases for async mode are fetching token metadata from IPFS or fetching the token decimals for an erc20 token, but ultimately you can do anything inside an asynchronous handler. The next handler will only run once that promise has returned.
The process of converting your syncronous handler to use async-mode
requires 3 things:
- Add the
isAsync: true
to theconfig.yaml
of any event handler that you want to be asynchronous. - Rename your
handler
function tohandlerAsync
. In TypeScript, this is<contract-name>Contract_<event-name>Event_handlerAsync
. In JavaScript and ReScript, it is<contract-name>Contract.<event-name>Event.handlerAsync
. Full examples below. - Make the handler function a promise, and "await" any
get
orget<linked-entity>
calls on the context since they are promises too in async mode.
Implications of asynchronous mode
Simply put, using asynchronous mode too much will slow down your indexer. If speed is not a big concern, you can use async mode more liberally.
Regardless, if possible we recommend building the indexer with only blockchain events; and if you are using async mode, try to use it only for the most important things that you need to fetch. If you are still in the process of developing your smart contracts we highly recommend adding more logs to your smart-contract codebase with more data so that you never have to reach for async mode to fetch onchain data.
With async-mode, loaders
become optional.
If you forget to write them or add your entity to the loader, it will be loaded asyncronously from the database.
Once again, this has a performance cost.
We ask that you use asynchronous mode with caution. For example, if you forget to return your promise in your handler there could be unexpected behaviours since the next event might start getting processed before your async function has completed. The other issue is that if an external promise fails it could totally block your indexer (eg. if a 3rd party API is unreachable for example).
Example
As an example, we will modify the Greeter
template (which can be generated with envio init template -t greeter
command) to send the user who makes the greeting an address registry to get the ENS name of the user if it exists and generate an AI response message back.
The two asynchronous functions we'll use are getEnsNameIfAvailable
and generateAIResponse
.
NOTE: _For simplicity we aren't doing any kind of error handling on the async functions. Ladies and gentlemen, handle your errors or the resulting frustration is on you.
config.yaml:
name: Greeter
description: Greeter indexer
contracts:
- name: Greeter
handler: ./src/EventHandlers.js
events:
- event: "NewGreeting(address user,string user)"
+ isAsync: true
schema.graphql (entity schema):
type User {
id: ID!
greetings: [String!]!
latestGreeting: String!
numberOfGreetings: Int!
+ aiResponse: String!
}
handlers:
- Javascript
- Typescript
- Rescript
GreeterContract.NewGreeting.handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;
const usersEnsOrAddress = await getEnsNameIfAvailable(userId);
const aiResponse = await generateAIResponse(
usersEnsOrAddress,
latestGreeting
);
const currentUserEntity = await context.User.get(userId);
const userEntity: UserEntity = currentUserEntity
? {
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
aiResponse,
}
: {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
aiResponse,
};
context.User.set(userEntity);
});
And here is a diff to highlight the change:
- GreeterContract_NewGreeting_handler(({ event, context }) => {
+ GreeterContract_NewGreeting_handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;
+ const usersEnsOrAddress = await getEnsNameIfAvailable(userId)
+ const aiResponse = await generateAIResponse(usersEnsOrAddress, latestGreeting)
- const currentUserEntity = context.User.get(userId);
+ const currentUserEntity = await context.User.get(userId);
const userEntity: UserEntity = currentUserEntity
?
{
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
+ aiResponse,
}
:
{
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
+ aiResponse,
};
context.User.set(userEntity);
});
GreeterContract_NewGreeting_handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;
const usersEnsOrAddress = await getEnsNameIfAvailable(userId);
const aiResponse = await generateAIResponse(
usersEnsOrAddress,
latestGreeting
);
const currentUserEntity = await context.User.get(userId);
const userEntity: UserEntity = currentUserEntity
? {
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
aiResponse,
}
: {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
aiResponse,
};
context.User.set(userEntity);
});
And here is a diff to highlight the change:
- GreeterContract_NewGreeting_handler(({ event, context }) => {
+ GreeterContract_NewGreeting_handlerAsync(async ({ event, context }) => {
const userId = event.params.user;
const latestGreeting = event.params.greeting;
+ const usersEnsOrAddress = await getEnsNameIfAvailable(userId)
+ const aiResponse = await generateAIResponse(usersEnsOrAddress, latestGreeting)
- const currentUserEntity = context.User.get(userId);
+ const currentUserEntity = await context.User.get(userId);
const userEntity: UserEntity = currentUserEntity
?
{
id: userId,
latestGreeting,
numberOfGreetings: currentUserEntity.numberOfGreetings + 1,
greetings: [...currentUserEntity.greetings, latestGreeting],
+ aiResponse,
}
:
{
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
+ aiResponse,
};
context.User.set(userEntity);
});
Handlers.GreeterContract.NewGreeting.handlerAsync(async ({ event, context }) => {
let userId = event.params.user->Ethers.ethAddressToString
let latestGreeting = event.params.greeting
let usersEnsOrAddress = await getEnsNameIfAvailable(userId)
let aiResponse = await generateAIResponse(usersEnsOrAddress, latestGreeting)
let maybecurrentUserEntity = await context.User.get(userId)
let userEntity: userEntity = switch maybecurrentUserEntity {
| Some(existingUserEntity) => {
id: userId,
latestGreeting,
numberOfGreetings: existingUserEntity.numberOfGreetings + 1,
greetings: existingUserEntity.greetings->Belt.Array.concat([latestGreeting]),
aiResponse,
}
| None => {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
aiResponse,
}
}
context.User.set(userEntity)
})
And here is a diff to highlight the change:
- Handlers.GreeterContract.NewGreeting.handler(({ event, context }) => {
+ Handlers.GreeterContract.NewGreeting.handlerAsync(async ({ event, context }) => {
let userId = event.params.user->Ethers.ethAddressToString
let latestGreeting = event.params.greeting
+ let usersEnsOrAddress = await getEnsNameIfAvailable(userId)
+ let aiResponse = await generateAIResponse(usersEnsOrAddress, latestGreeting)
- let maybecurrentUserEntity = context.User.get(userId)
+ let maybecurrentUserEntity = await context.User.get(userId)
let userEntity: userEntity = switch maybecurrentUserEntity {
| Some(existingUserEntity) => {
id: userId,
latestGreeting,
numberOfGreetings: existingUserEntity.numberOfGreetings + 1,
greetings: existingUserEntity.greetings->Belt.Array.concat([latestGreeting]),
aiResponse,
}
| None => {
id: userId,
latestGreeting,
numberOfGreetings: 1,
greetings: [latestGreeting],
aiResponse,
}
}
context.User.set(userEntity)
})
Testing
Testing async functions can be tricky if you have 3rd party dependencies. We would recommend mocking those with your mocking library of choice.
In the testing framework itself, the only change is that processEvent
is now processEventAsync
, and it returns a promise rather than being synchronous.