Some examples of edge cases that have to be watched out for in GetEventStore, starting with the read API. Here's the F#-API type for ReadStreamEventsForwardAsync:
/// <see cref="EventStore.ClientAPI.StreamEventsSlice" />
type StreamEventsSlice =
| NotFound of StreamId
| Deleted of StreamId
| Success of StreamSlice
/// Convert a <see cref="EventStore.ClientAPI.StreamEventsSlice" /> to a
/// ConnectionApi.EventsSlice.
and StreamSlice =
| StreamSlice of Slice
| EndOfStream of Slice
and Slice =
{ Events : ResolvedEvent list
; Stream : StreamId
; FromEventNumber : uint32
; ReadDirection : ReadDirection
; NextEventNumber : uint32
; LastEventNumber : uint32 }
and StreamId = string
and StreamId = stringHere's the corresponding decompiled API source:
public class StreamEventsSlice
{
public readonly SliceReadStatus Status;
public readonly string Stream;
public readonly int FromEventNumber;
public readonly ReadDirection ReadDirection;
public readonly EventStore.ClientAPI.ResolvedEvent[] Events;
public readonly int NextEventNumber;
public readonly int LastEventNumber;
public readonly bool IsEndOfStream;
internal StreamEventsSlice(SliceReadStatus status, string stream, int fromEventNumber, ReadDirection readDirection, ClientMessage.ResolvedIndexedEvent[] events, int nextEventNumber, int lastEventNumber, bool isEndOfStream)
{
Ensure.NotNullOrEmpty(stream, "stream");
this.Status = status;
this.Stream = stream;
this.FromEventNumber = fromEventNumber;
this.ReadDirection = readDirection;
if (events == null || events.Length == 0)
{
this.Events = Empty.ResolvedEvents;
}
else
{
this.Events = new EventStore.ClientAPI.ResolvedEvent[events.Length];
for (int index = 0; index < this.Events.Length; ++index)
this.Events[index] = new EventStore.ClientAPI.ResolvedEvent(events[index]);
}
this.NextEventNumber = nextEventNumber;
this.LastEventNumber = lastEventNumber;
this.IsEndOfStream = isEndOfStream;
}
}Leaving aside the pain of having public APIs with internal c'tors (forcing this darling* to come to life), it is clear there are some implicit invariants:
- We might get IsEndOfStream = true and then all other properties are pretty much possible to ignore. This has to be abducted.
- The stream can both be not found (Status = StreamReadStatus.NotFound) and,
- previously deleted (Status = StreamReadStatus.Deleted) or it can be
- Found, having data.
Besides that, the call to ReadStreamEventsForwardAsync has two System.Int32 parameters, which always have to be zero or a positive number. Why not use System.UInt32? Also, passing a zero count makes no sense, so that should really be always a positive number.
It becomes even more interesting when passing expectedVersion and to tell the API -2 (int) for no optimistic concurrency checking, -1 for NoStream, 0 for empty stream, having runtime exceptions in all other negative cases, and on top of that, having a possibility of getting WrongExpectedVersionException as an exception on the asynchronous call, if the optimistic check doesn't pass.
footnote:
// returns the constructor for the instance and a setter function, as a tuple, respectively
let reflPkgFor<'a> () : (unit -> 'a) * ('a -> string -> obj -> unit) =
let setterFor name = typeof<'a>.GetField(name, System.Reflection.BindingFlags.Instance ||| System.Reflection.BindingFlags.NonPublic)
let memSetterFor = memoize setterFor
let emptyCtor = fun () -> System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof<'a>) :?> 'a
let setter (instance : 'a) prop (value : obj) = memSetterFor prop |> (fun fi -> fi.SetValue(instance, value))
emptyCtor, setter
First of all, this piece is wrong:
and StreamSlice =
| StreamSlice of Slice
| EndOfStream
StreamSlice can have events AND can have IsEndOfStream set as well. IsEndOfStream means that by the time the read request was served, there were no more events in stream except those returned in Slice (you could have zero events returned as well).
IsEndOfStream is indicator, that there were no more events to read in that direction, it doesn't make LastEventNumber or NextEventNumber wrong (or not needed). Plus, in case of using $maxCount, NextEventNumber allows you to skip a lot of scavenged events (due to $maxCount constraint) and not page empty slices through all streams. It is not that smart with $maxAge, but in the future we can make this more intelligent.
As an example, suppose you have a stream sample-stream with $maxCount set to 20, but you've already pushed 1mln events there. When you start reading with ReadStreamEventsForward(Async) from 0 with maxCount 100, you'll get an empty slice with NextEventNumber 999900 and can skip almost 10000 empty reads.
When you are doing backward read, IsEndOfStream will tell you not to continue reading from sample-stream after first read, because you are guaranteed to not receive any more data.