Docs / ArchitectureKit 2.0 / Guides / Reading Events

Reading Events

Some reads need the events themselves rather than a state, and fit neither Load nor a projection, for example one page of a long history, the events up to a certain one, or a single event. To read the events as they are stored, call the Read function with a context, the store, and the subjects to read. Say which subjects with the SubjectTree or the ExactSubject function, as for a projection (see Running Projections).

It returns an iterator over the events and errors:

for event, err := range architecturekit.Read(context.TODO(), store, architecturekit.ExactSubject("/books/42")) {
  if err != nil {
    // ...
  }

  // ...
}

Without options, Read hands out every event of the subjects, oldest first. To read fewer of them, or in the other order, hand over options:

  • FromEvent reads from the event with the given ID on, including it, and AfterEvent reads the events after it.
  • UpToEvent reads up to the event with the given ID, including it, and BeforeEvent reads the events before it.
  • NewestFirst hands out the newest event first. The bounds stay what they are.
  • FromLatestEvent starts at the latest event of a type on a subject, including it, as FromLatest does for a state (see Reading Long Streams). If the subject has no such event, ReadEverything reads all events, and ReadNothing reads none.

For example, to read the next page of the history of a book, hand over the ID of the last event of the page before:

events := architecturekit.Read(context.TODO(), store, architecturekit.ExactSubject("/books/42"),
  architecturekit.AfterEvent(lastEventID),
)

To read the history of a book from its latest audit on, up to a certain event:

events := architecturekit.Read(context.TODO(), store, architecturekit.ExactSubject("/books/42"),
  architecturekit.FromLatestEvent("/books/42", BookAudited{}.EventType(), architecturekit.ReadEverything),
  architecturekit.UpToEvent(eventID),
)

The IDs are strings, as everywhere else in the kit. The database hands them out as one ascending sequence across all subjects, so a bound does not have to be an event of the subjects that are read. An ID that is not the one of an event, such as abc or an empty one, ends the iteration with an error that wraps ErrNotARevision and names the ID, as in not a revision: "abc", before the database is asked. That way, an ID that comes from a request can be told apart from a failure of the database, and the httpapi package answers it with 400 Bad Request and the error as the message (see Mapping Errors to Status Codes).

Note that a read has at most one lower bound, one upper bound, and one order, and that FromLatestEvent counts as a lower bound. Options that contradict each other, such as FromEvent together with AfterEvent, or NewestFirst given twice, make Read panic. So does FromLatestEvent together with NewestFirst, since the database reads from the latest event of a type only oldest first, and so does the zero value of Subjects, or a nil option. A subject for FromLatestEvent that does not start with a slash makes FromLatestEvent itself panic, and so does a value other than ReadEverything and ReadNothing.

Note that FromLatestEvent looks for the event on the given subject alone, not below it, and that this subject does not have to be one of those that are read.

Bounds that leave no room for any event are a mistake, since no event can ever lie between them, such as BeforeEvent("0"), AfterEvent and BeforeEvent with two neighboring IDs, a lower bound above the upper one, or AfterEvent of the largest revision, 9223372036854775807, after which no event can ever come. They end the iteration with an error that wraps ErrEmptyRange and names the values, before the database is asked, as in empty range: no event can lie after "0" and before "1" or empty range: no event can lie from "2" up to "1". The error names the values only, never the options, since a caller of an API does not know them, and the httpapi package answers it with 400 Bad Request and the error as the message (see Mapping Errors to Status Codes). A range that is only empty for now, on the other hand, such as the one after the last event written so far, is no mistake, since events can still come there, so Read hands out no events for it, without an error. So a reader that pages backwards with BeforeEvent and NewestFirst stops at a page that ends with the event 0, since no event can lie before it.

Changed in 2.0Note that the lower bound of FromLatestEvent is only known once the database has found the latest event of the type, so together with it, only an upper bound that leaves no room on its own, BeforeEvent("0"), is refused that way. The database refuses an upper bound before the latest event of the type, and Read then fails with an error of the category ErrPermanent, since trying again never helps, and its message keeps the description the database gives.

To check the options of a read this way without reading, call the CheckReadOptions function with the options, which returns the same error, or nil, as CheckPreconditions does for the preconditions of a command (see Using Preconditions). That way, bounds that come from a request can be refused before doing something that must not happen for a read that Read refuses, for example recording who reads the history of a book:

options := []architecturekit.ReadOption{
  architecturekit.AfterEvent(after),
  architecturekit.BeforeEvent(before),
}

if err := architecturekit.CheckReadOptions(options...); err != nil {
  // ...
}

// Record who reads the history of the book.

events := architecturekit.Read(context.TODO(), store, architecturekit.ExactSubject("/books/42"), options...)

Note that CheckReadOptions panics where Read does, for options that contradict each other and for a nil option, before it checks any ID. It takes no subjects, since none of the checks depends on them, so only Read panics for the zero value of Subjects. Options that pass can still make the database refuse the read, such as an upper bound before the latest event of the type of FromLatestEvent.

Every event is verified, like everything else the store reads, before the loop sees it (see Verifying Events). A failure belongs to a category, as with Load (see Handling Errors), and ends the iteration. The store stops reading as soon as the loop ends, so breaking out of it after a page is fine.

The events come as they are stored, without upcasters or rules, since there is no state. To get at the data of an event, call the Decode function (see Executing Commands).

For anything that Read does not offer, call the ReadEvents function of the client SDK, which takes the options of the database as they are. The events it hands out are not verified then, and a failure belongs to no category.