Skip to main content

Using state payloads

caution

This tutorial is a follow-up to the introduction to event payloads, 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 discussed event payloads, in this one we will be discussing state payloads.

Once more, consider our imaginary company, where despite the funding helping the sensors, we now need to track how much water we're using on the plant!

Extending the states

To keep track of the spent water, we need to keep it through every state, rather than a single one like we did for the event.

We can however use a single type for the payload:

src/robot.ts
type SpentWater = {
/// Tracks the latest amount of water used
lastMl: number
/// Tracks the total amount of water used
totalMl: number
}

As for the states, we will need to stop using designEmpty and use designState and withPayload instead:

- export const Idle = machine.designEmpty("Idle").finish()
- export const WateringPlant = machine.designEmpty("WateringPlant").finish()
+ export const Idle = machine
+ .designState("Idle")
+ .withPayload<SpentWater>()
+ .finish();
+ export const WateringPlant = machine
+ .designState("WateringPlant")
+ .withPayload<SpentWater>()
+ .finish();

Similarly to the previous tutorial, we broke a bunch of other places in the code.

Open to see the expected compilation errors.
$ npm run start-robot

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

src/robot.ts:41:24 - error TS2554: Expected 1 arguments, but got 0.

41 return WateringPlant.make();
~~~~~~

node_modules/@actyx/machine-runner/lib/esm/design/state.d.ts:122:12
122 make: (payload: StatePayload) => StatePayload;
~~~~~~~~~~~~~~~~~~~~~
An argument for 'payload' was not provided.

src/robot.ts:44:58 - error TS2554: Expected 1 arguments, but got 0.

44 WateringPlant.react([Events.HasWater], Idle, (_) => Idle.make())
~~~~~~

node_modules/@actyx/machine-runner/lib/esm/design/state.d.ts:122:12
122 make: (payload: StatePayload) => StatePayload;
~~~~~~~~~~~~~~~~~~~~~
An argument for 'payload' was not provided.

src/robot.ts:55:50 - error TS2345: Argument of type 'StateFactory<"wateringRobot", "robot", Factory<"HasWater", Record<never, never>> | Factory<"NeedsWater", NeedsWaterPayload>, "Idle", SpentWater, Record<...>>' is not assignable to parameter of type 'StateFactory<"wateringRobot", "robot", Factory<"HasWater", Record<never, never>> | Factory<"NeedsWater", NeedsWaterPayload>, any, SpentWater | undefined, any>'.
Types of property 'make' are incompatible.
Type '(payload: SpentWater) => SpentWater' is not assignable to type '(payload: SpentWater | undefined) => SpentWater | undefined'.
Types of parameters 'payload' and 'payload' are incompatible.
Type 'SpentWater | undefined' is not assignable to type 'SpentWater'.
Type 'undefined' is not assignable to type 'SpentWater'.

55 const machine = createMachineRunner(sdk, tags, Idle, undefined)
~~~~


Found 3 errors in the same file, starting at: src/robot.ts:41

To fix them, we need to correct the creation of states in our react calls.

Fixing the errors

We'll start by fixing the Idle.react call:

src/robot.ts
Idle.react([Events.NeedsWater], WateringPlant, (state, event) => {
console.log(`The plant is requesting ${event.payload.requiredWaterMl} ml of water!`)
const newStatePayload = {
lastMl: event.payload.requiredWaterMl,
totalMl: state.self.totalMl + event.payload.requiredWaterMl,
}
return WateringPlant.make(newStatePayload)
})

We've replaced the _ on our handler with state, we are now returning WateringPlant.make(newStatePayload) instead of just WateringPlant.make().

Our newStatePayload is also a mix between new properties and the old ones from the previous state, we needed to replace the lastMl with the new amount from the event, and we needed to increase our spent water consumption (and yes, we could do this in the transition from WateringPlant to Idle but didn't for the sake of simplicity).

Moving on to the transition from WateringPlant to Idle, we need a simpler change:

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

Notice that we're using the first argument of the handler, while for events we have previously used the second. The first one is the ReactionContext which contains the current state payload (i.e. before the transition happens) under self.

We're not applying changes to the state since we've delegated all updates to the previous transition, so in this case, we're just keeping the state as is.

The last error on our list is a bit more opaque:

src/robot.ts:55:50 - error TS2345: Argument of type 'StateFactory<"wateringRobot", "robot", Factory<"HasWater", Record<never, never>> | Factory<"NeedsWater", NeedsWaterPayload>, "Idle", SpentWater, Record<...>>' is not assignable to parameter of type 'StateFactory<"wateringRobot", "robot", Factory<"HasWater", Record<never, never>> | Factory<"NeedsWater", NeedsWaterPayload>, any, SpentWater | undefined, any>'.
Types of property 'make' are incompatible.
Type '(payload: SpentWater) => SpentWater' is not assignable to type '(payload: SpentWater | undefined) => SpentWater | undefined'.
Types of parameters 'payload' and 'payload' are incompatible.
Type 'SpentWater | undefined' is not assignable to type 'SpentWater'.
Type 'undefined' is not assignable to type 'SpentWater'.

55 const machine = createMachineRunner(sdk, tags, Idle, undefined);
~~~~

Notice how it points to Idle and it mentions:

(payload: SpentWater) => SpentWater is not assignable to type (payload: SpentWater | undefined) => SpentWater | undefined

All this but our handlers return SpentWater not SpentWater | undefined, nor do they take the latter. The fix is simpler than this, and astute readers may have detected the issue already.

It lies in the last parameter of createMachineRunner which is the payload for the initial state, since we change the Idle payload to be SpentWater we can't pass it undefined.

To fix it, we simply need to pass an initial SpentWater payload to it:

src/robot.ts
const machine = createMachineRunner(sdk, tags, Idle, {
lastMl: 0,
totalMl: 0,
})
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'

type SpentWater = {
lastMl: number
totalMl: number
}

const machine = protocol.makeMachine('robot')

export const Idle = machine.designState('Idle').withPayload<SpentWater>().finish()

export const WateringPlant = machine.designState('WateringPlant').withPayload<SpentWater>().finish()

Idle.react([Events.NeedsWater], WateringPlant, (state, event) => {
console.log(`The plant is requesting ${event.payload.requiredWaterMl} ml of water!`)
const newStatePayload = {
lastMl: event.payload.requiredWaterMl,
totalMl: state.self.totalMl + event.payload.requiredWaterMl,
}
console.log(`Total water consumption: ${newStatePayload.totalMl}`)
return WateringPlant.make(newStatePayload)
})

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, {
lastMl: 0,
totalMl: 0,
})

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

main()

Running our new robot!

Before we run our robot, let's add some logs to track our progress and request water more than once.

On robot.ts:

  Idle.react([Events.NeedsWater], WateringPlant, (state, event) => {
console.log(
`The plant is requesting ${event.payload.requiredWaterMl} ml of water!`
)
const newStatePayload = {
lastMl: event.payload.requiredWaterMl,
totalMl: state.self.totalMl + event.payload.requiredWaterMl,
}
+ console.log(`Total water consumption: ${newStatePayload.totalMl}`)
return WateringPlant.make(newStatePayload)
})

And on sensor.ts:

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

+ for (let i = 0; i < 3; i++) {
await sdk.publish(
tags.apply(
Events.NeedsWater.make({
requiredWaterMl: Math.floor(Math.random() * 100),
})
)
);
console.log("Published NeedsWater");

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

sdk.dispose();
}

Now we can run the robot and sensor like we have been doing.

You'll see a similar output on your robot (remember that the amount of water is random).
$ npm run start-robot

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

{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 0, totalMl: 0 },
type: 'Idle'
}
The plant is requesting 45 ml of water!
Total water consumption: 45
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 45, totalMl: 45 },
type: 'WateringPlant'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 45, totalMl: 45 },
type: 'Idle'
}
The plant is requesting 69 ml of water!
Total water consumption: 114
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 69, totalMl: 114 },
type: 'WateringPlant'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 69, totalMl: 114 },
type: 'Idle'
}
The plant is requesting 41 ml of water!
Total water consumption: 155
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 41, totalMl: 155 },
type: 'WateringPlant'
}
{
is: [Function: is],
as: [Function: as],
cast: [Function: cast],
payload: { lastMl: 41, totalMl: 155 },
type: 'Idle'
}

Conclusion

In this tutorial we have covered state payloads, how to create and use them. This concludes the introductory tutorials about Machine Runner!