Snapshots

The state of a fish may become expensive to calculate from scratch: snapshots to the rescue!

Actyx Pond supports two types of snapshots to avoid processing all known events for a fish’s subscription set during wakeup: state snapshots and semantic snapshots.

State snapshots

Note

State snapshots are currently called “local snapshots” since in contrast to semantic snapshots they are bound to a device. This restriction will be lifted in a future version of Actyx Pond for distributed fishes that consume identical subscription sets when instantiated on different devices.

The chat room fish in our example keeps a list of messages in its state. In order to keep its wakeup time constant, we can write this list into a snapshot from time to time, so that not the full log needs to be replayed when the app starts. Instead, Actyx Pond will load the latest stored snapshot and start from there, replaying only events that come after the snapshot. The best part about this is that also the writing is done by Actyx Pond, we only need to switch on snapshots as a configuration option:

export const chatRoomFish = FishType.of({
  // ... same as before
  localSnapshot: {
    version: 1,
    serialize: state => state,
    deserialize: json => json as string[],
  },
})

The serialize function transforms the state of the fish into something that can be serialized with JSON.stringify() — since our state is just an array of strings, this is already the case. The result of serialization will come back into the deserialize function when the time comes to read a snapshot. In this case we know that we produced an array of strings, so we can just cast the value.

This is of course only possible if we keep the serialized format the same. For this purpose the snapshot has a version number as well that we have set to 1 here. Upon every change to the necessary interpretation of the serialized data format, the version number needs to be incremented. When that happens, all old snapshots are invalidated and the newly written ones will have the new version number.

With this configuration Actyx Pond will take one snapshot per hour and retain them on a daily, monthly, and yearly raster, i.e. a long-running fish will have up to four snapshots stored on the device where it is running.

Why is it necessary to keep multiple snapshots? It may happen that a device that still has event stored for this fish lies disconnected in a drawer for a month, and when it comes back online it will synchronize these events, leading the fish to time travel to a state before those events. The state from a month ago will probably no longer be cached in memory, so a full replay is started, taking advantage of any snapshot that is older than the event that caused the time travel.

Semantic snapshots

Some fishes have events that completely determine the state after applying said event. Consider as an example the ability to wipe the chat room clean with a new command.

type ChatRoomCommand = { type: 'addMessage'; message: string } | { type: 'clearAllMessage' }
type ChatRoomEvent = { type: 'messageAdded'; message: string } | { type: 'messagesCleared' }

The result of applying the messagesCleared event would be the empty array. Therefore it would not make sense to replay any prior events, their effect would be undone by this event. Actyx Pond can be informed about this by enabling the semantic snapshot configuration option when defining the fish type:

export const chatRoomFish = FishType.of({
  // ... same as before
  semanticSnapshot: (_name, _sourceId) => event => event.payload.type === 'messagesCleared',
})

The supplied function computes an event predicate from the fish name and device source ID. Actyx Pond will during a replay search backwards through the event log, from youngest event to oldest, until an event is found that matches the predicate. This event will then be applied to the initial state of the fish, followed by all succeeding events from the log.

Note

Whether an event constitutes a semantic snapshot lies in the eye of the beholder: the chat room fish may consider the messagesCleared of its event stream as such an event, but another fish listening to the same event stream may not (e.g. if it shall count all messages ever posted to the chat room). Therefore, the semanticSnapshot property is defined by the fish type and not by the event type.

Both kinds of snapshots can be combined within the same fish as well.