How to consume events without using fish

I just realized that it is possible to use events without using fish (from the SHT module in the Actyx academy, where there is actually no machineFish, the OPCUA connector just query the events directly from the pond)

I am trying to use such pattern to make a UI to control a hardware (in this case, let’s say locking a door).
For the UI I am using React pond:

import { usePond } from '@actyx-contrib/react-pond'
import { Tag } from '@actyx/pond'
import React from 'react'

export const doorLockRequestSentEvent = {
  eventType: 'doorLockRequestSentEvent',
  machineId: 1,
}

export const DoorControl = (): JSX.Element => {
  const pond = usePond()

  const lockTheDoor = () => {
    pond.emit(Tag('machine'), doorLockRequestSentEvent)
  }
  return (
    <div>
      <button className="btn btn-primary" onClick={lockTheDoor}>
        Lock the door
      </button>
    </div>
  )
}

the door controller is something like this:

import { Pond, Tag } from '@actyx/pond'
Pond.default(manifest).then((pond) => {

  const orphanEvent = pond.events()
  // listen to the event --> how?
  // then execute lockDoor()

 function lockDoor(){
//already implemented
  }
)}

I am not sure which function I should use from the pond.events() and how to use it.
I hope the question makes sense. Thank you in advance!

Hi @Dipta-IIC-EU,

indeed, you can listen to the events to do a specific task. In this example it would be something like:

pond.events().observeLatest(
  {
    query: Tag('machine'),
    eventOrder: EventOrder.Lamport,
  },
  (event, metaData) => {
    doSomething()
  },
)

Please keep in mind, that all events eventually arrive. Even if it is two days later.

In addition to that, you will always get the last event when you start to observeLatest(). That means that your door will always be locked after a reboot.


You could get around all this issues with a local-twin / fish.
Create two events.

  • LockRequested: somebody in the system likes to look the door
  • DoorLocked: The door recognized the request and actually locked the door.

And we could do the same for unlocking the door:

  • UnlockRequested: somebody in the system likes to unlock the door
  • DoorUnlooked: The door got unlooked with a key

The state of the local twin would look very similar:

  • unlockRequested
  • unlocked
  • lockRequested
  • locked

have that, you only have to observe the DoorFish and react to the state changes.

    pond.observe(DoorFish.of(doorName), (state) => {
      switch (state) {
        case 'unlockRequested':
          await lockDoor(doorName)
          emitDoorLockedEvent(pond, doorName)
          break
        case 'lockRequested':
          await unlockDoor(doorName)
          emitDoorUnlockedEvent(pond, doorName)
          break
      }
    })

If you observe a door, that is already locked, you will not lock it again.

I hope this shows you both ways how you could do this.

Hi @alex_AX thank you for the detailed reply.
In case I would like to use fish for the above case, how would the DoorFish's onEvent look like?
As the door-lock and -unlock function has been called in the above pond.observe, so for the onEvent we just need to update the fish’s state accordingly, is my understanding correct?

onEvent: (state, event) => {
      switch (event.eventType) {   
         case 'doorLockedEvent':
            state.doorLockStatus = true
            break

Yes, you got it right.
To update the state you have to emit an event of the happening. “the door is locked not” ==> DoorLockedEvent. and the fish will process this event and reaches the locked state.


You could make your fish much simpler:

type State = 'unknown' | 'unlocked' | 'locked' | 'unlockRequested' | 'lockRequested'
type DoorLockRequestedEvent = { eventType: 'doorLockRequested'; machineId: number }
type DoorLockedEvent = { eventType: 'doorLocked'; machineId: number }
type DoorUnlockRequestedEvent = { eventType: 'doorUnlockRequested'; machineId: number }
type DoorUnlockedEvent = { eventType: 'doorUnlocked'; machineId: number }
type Event = DoorLockRequestedEvent | DoorLockedEvent | DoorUnlockRequestedEvent | DoorUnlockedEvent

const doorTag = Tag<Event>('machine')
const DoorFish = {
  of: (name: string): Fish<State, Event> => ({
    fishId: FishId.of('example.doorFish', name, 0),
    initialState: 'unknown',
    where: doorTag.withId(name),
    onEvent: (_state, event) => {
      switch (event.eventType) {
        case 'doorLockRequested':
          return 'lockRequested'
        case 'doorUnlockRequested':
          return 'unlockRequested'
        case 'doorLocked':
          return 'locked'
        case 'doorUnlocked':
          return 'unlocked'
      }
    },
  }),
}

It just represents the current state of the door if it is unknown | unlocked | locked | unlockRequested | lockRequested

Thank you, your answer is very clear!

One additional question: is my understanding below correct?
Looking at the examples including the one above, the functions inside onEvent should only affect the state of the fish, and nothing else. So for example we should not define/call the actual function that locks the door, directly from inside the fish’s onEvent. Instead, use Pond.observe from the application.

One more little thing I’d like to add: figuring out what to do (based on Pond.observe()) may also check the current time — this is only forbidden in onEvent. You can use that to reject requests that are too old.

Yes, you understand this correctly.

The fish will replay the events each time you restart the app or an event appears from a partitioned node that needs to be sorted in the right order.

It is very important, that you have nod side effects in your onEvent function. Please do your application logic in pond.observe(), pond.run(), or pond.keepRunning()

1 Like

@roland and @alex_AX
thank you!