Skip to content

Latest commit

 

History

History
98 lines (76 loc) · 4.33 KB

README.md

File metadata and controls

98 lines (76 loc) · 4.33 KB

This document briefly describes the event log and history APIs, which can be used to store a sequence of events with their aggregated value.

The low-level event log API

Event logs

A simple event log

The event log is part of the core library. It's possible to merge multiple event logs using the dedicated merge() function, and efficiently iterate over all the events, or over the events that were issued by a specific site.

In most cases, manually interacting with the event log won't be useful. However, in some special cases, you can optimize the event log and remove operations which aren't necessary to converge.

For instance, last-write-wins registers work this way : the values associated with the previous writes to the register can be safely discarded.

The usage of an EventLog or a MutableEventLog somehow mimics the standard behavior of collections :

// A SiteIdentifier uniquely identifies a site in the library. They can be constructed with an
// instance of Random.
val alice = Random.nextSiteIdentifier()
val bob = Random.nextSiteIdentifier()

// Events are just arrays of bytes. You should use the [echo] library if you're interested in
// working with generic types, which will automatically be serialized.
val event1: ByteArray = ..
val event2: ByteArray = ..
val event3: ByteArray = ..

val log = mutableEventLogOf().apply {
    // Appending an event means that the event will get a higher sequence number than all the
    // previously issued events for the given site. This is usually what you'll want to do when
    // issuing some new events.
    //
    // In this example, we have `seq1 == SequenceNumber.Min`, and `seq2 == SequenceNumber.Min + 1u`.
    val (seq1, _) = append(site = alice, event = event1)
    val (seq2, _) = append(site = alice, event = event2)

    // Inserting an event lets you specify both the site identifier and the sequence number for an
    // event. However, you can't insert events for a given site if the log has already seen a
    // sequence number greater or equal to it.
    insert(site = bob, seqno = SequenceNumber.Min, event = event3)
    insert(site = alice, seqno = SequenceNumber.Min, event = event1) // no effect
}

log.contains(SequenceNumber.Min + 0U, alice) // true
log.contains(SequenceNumber.Min + 2U, alice) // false

Check the sample out here.

Event logs work better with consecutive insertions of operations, since they use some gap buffers internally to store the events. You should therefore prefer calling merge() over consecutive insert() or append() of events.

Adding an aggregate

In most cases, you'll be interested in keeping an aggregated value associated with the head of your log. This is how all the CRDTs in this project are implemented : the event log acts as a source of truth for "what happened", and an aggregate is stored to indicate "what the current state is". The core library provides an abstraction for that : a MutableHistory<T>. It's actually very simple :

// file MutableHistory.kt
interface MutableHistory<out T> : MutableEventLog {
    val current: T
}

When creating a MutableHistory, you have to provide 3 things :

  • An initial value for the history, before any event is added to the log.
  • A forward function, that applies an event to the history.
  • A backward function, which undoes an event.

The forward and backward functions should be put in a MutableProjection<T> . You'll then be able to create a MutableHistory<T> instance :

val projection: MutableProjection<Int> = .. // Your own subclass of MutableProjection<Int>
val history = mutableHistoryOf(initial = 0, projection = projection)

// ... and use it exactly like an event log.
history.append(alice, byteArrayOf(1, 2, 3))