Skip to main content

Using event payloads

caution

This tutorial is a follow-up to the introduction to the Machine Runner, as such, we recommend that you go through it first. In the case you need the code from the previous tutorial, you can get from GitHub.

In the previous tutorial we ignored the required amount of water by the plant, in this tutorial we will address that by introducing payloads.

An event can contain additional information, a payload, which is useful to enrich the event's message. As an example, which we will use in this guide, a sensor can tell how much water a plant needs by including it as the event payload.

Enough talk, let's dive in!

Extending the event​

Our imaginary company got some funding and your boss upgraded the sensor, which can now tell how much water the plant needs, rather than just whether the plants needs water or not.

To handle this new sensor, we need to add a field tracking the amount of water to our NeedsWater event:

- export const NeedsWater = MachineEvent.design("NeedsWater").withoutPayload();
+ export const NeedsWater = MachineEvent.design("NeedsWater").withPayload<NeedsWaterPayload>();

Since we didn't declare the NeedsWaterPayload this won't work just yet, so lets add it as well:

src/protocol.ts
type NeedsWaterPayload = { requiredWaterMl: number }
Open to see the full contents of protocol.ts
src/protocol.ts
import { MachineEvent, SwarmProtocol } from '@actyx/machine-runner'

export const manifest = {
appId: 'com.example.tomato-robot',
displayName: 'Tomato Robot',
version: '1.0.0',
}

type NeedsWaterPayload = {
requiredWaterMl: number
}

export namespace Events {
export const HasWater = MachineEvent.design('HasWater').withoutPayload()
export const NeedsWater = MachineEvent.design('NeedsWater').withPayload<NeedsWaterPayload>()
export const All = [HasWater, NeedsWater] as const
}

export const protocol = SwarmProtocol.make('wateringRobot', Events.All)

We should be ok now, let's try to run our robot:

$ npm run start-robot

> [email protected] start-robot
> tsc && node dist/robot.js

src/sensor.ts:8:56 - error TS2345: Argument of type '{}' is not assignable to parameter of type 'NeedsWaterPayload'.
Property 'requiredWaterMl' is missing in type '{}' but required in type 'NeedsWaterPayload'.

8 await sdk.publish(where.apply(Events.NeedsWater.make({})));
~~

src/robot.ts:9:3
9 requiredWaterMl: number;
~~~~~~~~~~~~~~~
'requiredWaterMl' is declared here.

We can't run the robot because our sensor has a type mismatch, this makes sense since we've changed the payload type!

Fixing the sensor​

To fix the sensor we need to construct the proper type, luckily that's easy enough:

- await sdk.publish(tags.apply(Events.NeedsWater.make({})));
+ await sdk.publish(
+ tags.apply(
+ Events.NeedsWater.make({
+ requiredWaterMl: Math.floor(Math.random() * 100),
+ })
+ )
+ );
Open to see the full contents of sensor.ts
src/sensor.ts
import { Actyx } from '@actyx/sdk'
import { Events, manifest, protocol } from './protocol'

async function main() {
const sdk = await Actyx.of(manifest)
const tags = protocol.tagWithEntityId('robot-1')

let water = Math.floor(Math.random() * 100)
console.log(water)
await sdk.publish(
tags.apply(
Events.NeedsWater.make({
requiredWaterMl: water,
}),
),
)
console.log('Published NeedsWater')

await sdk.publish(tags.apply(Events.HasWater.make({})))
console.log('Published HasWater')

sdk.dispose()
}

main()

If we run the robot again:

$ npm run start-robot

> [email protected] start-robot
> tsc && node dist/robot.js

{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'Idle'
}

And if we run the sensor:

$ npm run start-sensor

> [email protected] start-sensor
> tsc && node dist/sensor.js

Published NeedsWater
Published HasWater

And look back into the robot, we should see the new states:

$ npm run start-robot

> [email protected] start-robot
> tsc && node src/robot.js

{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'Idle'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'WateringPlant'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'Idle'
}

This is a bit boring though we (the robot supervisor) don't know how much water was requested.

Let's do a bit better.

Checking the event payload​

To take a peek inside the event, we need to mess with our Idle.react, since it's the state that reacts to the NeedsWater event. To be precise, we need to extend the handler function:

- Idle.react([Events.NeedsWater], WateringPlant, (_) => WateringPlant.make())
+ Idle.react([Events.NeedsWater], WateringPlant, (_, event) => {
+ console.log(`The plant is requesting ${event.payload.requiredWaterMl} ml of water!`);
+ return WateringPlant.make();
+ });

The main change lies in the change from (_) => ... to (_, event) => ... which allows us to peek into the event. If you're using an editor with TypeScript support, your autocompletion engine might have event shown you options when placing a . in front of event.

Open to see the full contents of robot.ts
src/robot.ts
import { createMachineRunner } from '@actyx/machine-runner'
import { Actyx } from '@actyx/sdk'
import { Events, manifest, protocol } from './protocol'

const machine = protocol.makeMachine('robot')

export const Idle = machine.designEmpty('Idle').finish()
export const WateringPlant = machine.designEmpty('WateringPlant').finish()

Idle.react([Events.NeedsWater], WateringPlant, (_, event) => {
console.log(`The plant is requesting ${event.payload.requiredWaterMl} ml of water!`)
return WateringPlant.make()
})

WateringPlant.react([Events.HasWater], Idle, (state, _) => Idle.make(state.self))

export async function main() {
const sdk = await Actyx.of(manifest)
const tags = protocol.tagWithEntityId('robot-1')
const machine = createMachineRunner(sdk, tags, Idle, undefined)

for await (const state of machine) {
console.log(state)
}
}

main()

Now, when we run, we will see the required water level:

$ npm run start-robot

> [email protected] start-robot
> tsc && node dist/robot.js

{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'Idle'
}
The plant is requesting 86 ml of water!
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'WateringPlant'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: undefined,
type: 'Idle'
}

Tada! 🎉

Conclusion​

In this tutorial, we explored event payloads, namely, how to create them and refactor the required code to use them!