Docs / ArchitectureKit 1.0 / Fundamentals / Reading Your Own Writes

Reading Your Own Writes

A view lags behind the events that have been written, by however long its projection takes. To read your own writes, wait until the view has seen the events you have written.

The ID of the last event a view has seen is its revision. Since the database assigns event IDs in ascending order across all subjects, revisions can be compared.

A view sees only the events of the subjects its projection reads. So wait only for a write to one of those subjects: if the write went to another subject, the view does not reach its revision until a later event lands in one of its own subjects, and waiting for it lasts the full time, with WaitFor, with Await, and with the Wait-For-Revision header alike. WaitForWritten even fails then, with an error that wraps ErrNotCaughtUp, so hand it only the events written to the subjects the view reads.

Tracking Revisions#

To track the revision of a view, wrap the projection with the Tracking function and hand over the view. It records every event that reaches the projection, including the ones the projection ignores:

trackedProjection := architecturekit.Tracking(catalogProjection, catalog)

If the projection writes to several views, such as a libraryProjection that keeps both the catalog and the readers up to date (see Sharing Items with Readers), hand over all of them, so that each one knows how far it has come:

trackedProjection := architecturekit.Tracking(libraryProjection, catalog, readers)

Then run trackedProjection instead of catalogProjection (see Running Projections).

Tracking accepts every view that implements the RevisionSink interface, which consists of the Seen function. InMemoryView implements it. The interface is meant for a view that keeps no transaction, since Tracking records the revision once the projection has applied the event, apart from its data. Where data and revision have to become durable together, as in a view in a database, the revision belongs inside the transaction, so the projection has to write it itself.

Note that calling Tracking with nil as the projection, without any view, or with nil as one of the views panics. A nil pointer or a nil ProjectionFunc counts as nil, too, such as a view that was declared but never created with NewInMemoryView.

The tracked projection keeps the mode and the batch size of the projection it wraps. A transactional projection can not be tracked, since it has no Apply function. Record its revision within the transaction instead.

To get the revision your own write has produced, call the RevisionOf function with the written events. It returns the highest event ID, or an empty string if no events were written:

revision := architecturekit.RevisionOf(writtenEvents)

Waiting for Revisions#

To wait until a view has reached a revision, call the WaitFor function with a context and the revision. It returns nil once the view has reached the revision, at once if it has already. If the context ends first, it returns the error of the context, such as context.DeadlineExceeded, and for a value that is not a revision, it returns an error that wraps ErrNotARevision (see Comparing Revisions):

ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()

err := catalog.WaitFor(ctx, revision)
if err != nil {
  // ...
}

Note that running out of time is an error here. The Await function of the httpapi package, which waits for the revision an HTTP request asks for, takes it for none instead, and returns nil, so that the handler answers with what the view holds (see Waiting for a Revision in a Handler of Your Own).

To get the current revision of a view, call the Revision function with a context. It returns an empty string as long as the view has not seen any event, and an error if it can not read the revision, which may happen to a view of your own that keeps its revision in a database. InMemoryView keeps its revision in memory, so it never fails:

current, err := catalog.Revision(ctx)
if err != nil {
  // ...
}

Both functions form the Revisioned interface, which InMemoryView implements. To wait for revisions of a view of your own, implement it as well.

Comparing Revisions#

To compare two revisions, call the CompareRevisions function. Like cmp.Compare, it returns -1, 0, or 1. An empty revision comes before every other one. If a value is not a revision, it returns an error that wraps ErrNotARevision and names the value:

result, err := architecturekit.CompareRevisions("9", "10")
// result == -1

A revision is a decimal number from 0 to 2^63-1, the range of the event IDs of the database. To check a revision that comes from outside, such as the one a caller hands over, call the ParseRevision function. It reads the revision by the same rules, and returns the number it stands for. Unlike CompareRevisions, it refuses an empty revision as well, since that is the ID of no event, and so it is the function to check a revision with before it becomes part of a command (see Handling Commands over HTTP):

number, err := architecturekit.ParseRevision("42")
// number == 42

_, err = architecturekit.ParseRevision("")
// errors.Is(err, architecturekit.ErrNotARevision) == true

For a value that is not a revision, it returns an error that wraps ErrNotARevision and names the value, as CompareRevisions does.

Waiting for Written Events#

Sometimes a step on the server builds on what was just written, for example a handler that executes a command and then answers with what a view holds after it. To wait until a view has seen the events that Execute or Write returned, call the WaitForWritten function with a context, the view, the written events, and how long to wait at most:

writtenEvents, err := architecturekit.Execute(ctx, store, borrowBook, cmd)
if err != nil {
  // ...
}

err = architecturekit.WaitForWritten(ctx, catalog, writtenEvents, 5*time.Second)
if errors.Is(err, architecturekit.ErrNotCaughtUp) {
  // The command has succeeded, so answer with success all the same, and with
  // the revision, for which the caller can wait with Wait-For-Revision.
}

It waits with WaitFor for the revision that RevisionOf returns for the events, and returns nil once the view has reached it. If no events were written, it returns nil at once, since there is nothing to wait for. Every error it returns wraps ErrNotCaughtUp, since the events were written in any case, and says why the view has not caught up, keeping the cause, so that errors.Is and errors.As still find it:

  • If the time runs out first, the error says so, as in not caught up: the events were written, but the view did not catch up within 5s.
  • If the context ends first, it wraps the error of the context, as in not caught up: the events were written, but waiting for the view ended: context canceled, so errors.Is finds context.Canceled or context.DeadlineExceeded.
  • If the view fails otherwise, for example because a view in a database can not read its revision, it wraps the error of the view, as in not caught up: the events were written, but the view could not be asked: …, together with the category of that error, if it has one.

ErrNotCaughtUp belongs to no category, since the write has succeeded, and only the view lags behind: trying the write again would store its events twice, and the view may still catch up (see Handling Errors). So a handler that waits after a command usually answers with success when it gets ErrNotCaughtUp, as in the example above, and with the revision of the written events. If the caller needs to see what was written, it can wait for the revision with the Wait-For-Revision header of a query (see Reading Your Own Writes over HTTP). Answered as an error, StatusFor maps it to 500 Internal Server Error, rather than to a status that invites trying again, also if its cause is ErrTransient or the end of the context, and the message says that the request has succeeded (see Mapping Errors to Status Codes).

Note that the view sees only the events of the subjects its projection reads, so hand over only the events written to those subjects. For a write to several subjects, such as one with Write, the highest ID of all events may lie in a subject the view does not read, and then the view does not reach it until a later event lands in one of its own subjects (see Waiting for Revisions).

Note that running out of time is an error here, since the step needs what was written. The Await function of the httpapi package, on the other hand, waits for the revision a caller asks for, and takes running out of time for none, so that the handler answers with what the view holds (see Waiting for a Revision in a Handler of Your Own).

Note that WaitForWritten panics for a nil view, a nil pointer included, and for a timeout that is not positive, since a view that catches up with a write that has just happened needs some time, and waiting no time at all would fail almost always.