UNPKG

@codeforbreakfast/eventsourcing-store-inmemory

Version:

In-memory event store implementation for development and testing - Fast in-memory storage with complete EventStore interface implementation

339 lines (305 loc) 11.1 kB
import { Chunk, Effect, HashMap, Option, PubSub, Stream, SynchronizedRef, pipe } from 'effect'; import { EventStreamId, EventStreamPosition, ConcurrencyConflictError, type StreamEvent, } from '@codeforbreakfast/eventsourcing-store'; interface EventStream<V> { readonly events: Chunk.Chunk<V>; readonly pubsub: PubSub.PubSub<V>; } const emptyStream = <V>(): Effect.Effect<EventStream<V>, never, never> => pipe( 2 ^ 8, PubSub.bounded<V>, Effect.map((pubsub) => ({ events: Chunk.empty<V>(), pubsub, })) ); const createUpdatedEventStream = <V>( eventStream: EventStream<V>, newEvents: Chunk.Chunk<V> ): EventStream<V> => ({ events: pipe(eventStream.events, Chunk.appendAll(newEvents)), pubsub: eventStream.pubsub, }); const appendToExistingEventStream = <V = never>(position: EventStreamPosition, newEvents: Chunk.Chunk<V>) => (eventStream: EventStream<V>) => Effect.if(eventStream.events.length === position.eventNumber, { onTrue: () => Effect.succeed(createUpdatedEventStream(eventStream, newEvents)), onFalse: () => Effect.fail( new ConcurrencyConflictError({ expectedVersion: position.eventNumber, actualVersion: eventStream.events.length, streamId: position.streamId, }) ), }); const createOrAppendToStream = <V>(streamEnd: EventStreamPosition, newEvents: Chunk.Chunk<V>) => pipe(emptyStream<V>(), Effect.flatMap(appendToExistingEventStream(streamEnd, newEvents))); const logAppendedEvents = (newEvents: Chunk.Chunk<unknown>, streamEnd: EventStreamPosition) => Effect.annotateLogs(Effect.logInfo(`Appended events to stream`), { newEvents, streamEnd, }); const publishEventsToStream = <V>(pubsub: PubSub.PubSub<V>, newEvents: Chunk.Chunk<V>) => pipe(pubsub, PubSub.publishAll(newEvents)); const updateEventStreamsById = <V>( updatedEventStream: EventStream<V>, streamEnd: EventStreamPosition, newEvents: Chunk.Chunk<V> ) => (eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>) => pipe( eventStreamsById, HashMap.set(streamEnd.streamId, updatedEventStream), Effect.succeed, Effect.tap(() => logAppendedEvents(newEvents, streamEnd)), Effect.tap(() => publishEventsToStream(updatedEventStream.pubsub, newEvents)) ); const tagEventsWithStreamId = <V>( newEvents: Chunk.Chunk<V>, streamId: EventStreamId, startingEventNumber: number ) => pipe( newEvents, Chunk.map((event, index) => ({ position: { streamId, eventNumber: startingEventNumber + index }, event, })) ); const publishToAllEventsStream = <V>( allEventsStream: EventStream<StreamEvent<V>>, taggedEvents: Chunk.Chunk<StreamEvent<V>> ) => publishEventsToStream(allEventsStream.pubsub, taggedEvents); const createUpdatedValue = <V>( allEventsStream: EventStream<StreamEvent<V>>, newEvents: Chunk.Chunk<V>, streamEnd: EventStreamPosition ) => (eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>): Value<V> => { const taggedEvents = tagEventsWithStreamId( newEvents, streamEnd.streamId, streamEnd.eventNumber ); return { eventStreamsById, allEventsStream: { events: pipe(allEventsStream.events, Chunk.appendAll(taggedEvents)), pubsub: allEventsStream.pubsub, }, }; }; const applyUpdatedEventStreamToValue = <V>( allEventsStream: EventStream<StreamEvent<V>>, streamEnd: EventStreamPosition, newEvents: Chunk.Chunk<V>, eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>> ) => (updatedEventStream: EventStream<V>) => pipe( eventStreamsById, updateEventStreamsById(updatedEventStream, streamEnd, newEvents), Effect.map(createUpdatedValue(allEventsStream, newEvents, streamEnd)), Effect.tap((_value) => publishToAllEventsStream( allEventsStream, tagEventsWithStreamId(newEvents, streamEnd.streamId, streamEnd.eventNumber) ) ) ); const appendToEventStream = <V = never>(streamEnd: EventStreamPosition, newEvents: Chunk.Chunk<V>) => ({ eventStreamsById, allEventsStream, }: Value<V>): Effect.Effect<Value<V>, ConcurrencyConflictError, never> => pipe( eventStreamsById, HashMap.get(streamEnd.streamId), Option.match({ onSome: appendToExistingEventStream<V>(streamEnd, newEvents), onNone: () => createOrAppendToStream(streamEnd, newEvents), }), Effect.flatMap( applyUpdatedEventStreamToValue(allEventsStream, streamEnd, newEvents, eventStreamsById) ) ); const modifyEventStreamsWithEmptyIfMissing = <V>(streamId: EventStreamId, emptyStream: EventStream<V>) => (eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>) => HashMap.modifyAt( eventStreamsById, streamId, Option.match({ onNone: () => Option.some(emptyStream), onSome: Option.some, }) ); const modifyEventStreamsByIdWithEmptyStream = <V>( streamId: EventStreamId, emptyStream: EventStream<V>, eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>> ): Effect.Effect<HashMap.HashMap<EventStreamId, EventStream<V>>, never, never> => pipe( eventStreamsById, modifyEventStreamsWithEmptyIfMissing(streamId, emptyStream), Effect.succeed ); const buildValueFromEventStreamsById = <V>(allEventsStream: EventStream<StreamEvent<V>>) => (eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>): Value<V> => ({ eventStreamsById, allEventsStream, }); const ensureEventStream = <V = never>(streamId: EventStreamId) => ({ eventStreamsById, allEventsStream }: Value<V>): Effect.Effect<Value<V>, never, never> => pipe( emptyStream<V>(), Effect.flatMap((emptyStream) => modifyEventStreamsByIdWithEmptyStream(streamId, emptyStream, eventStreamsById) ), Effect.map(buildValueFromEventStreamsById(allEventsStream)) ); interface Value<V> { readonly eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>; readonly allEventsStream: EventStream<StreamEvent<V>>; } export interface InMemoryStore<V = never> { readonly append: ( to: EventStreamPosition ) => ( events: Chunk.Chunk<V> ) => Effect.Effect<EventStreamPosition, ConcurrencyConflictError, never>; readonly get: ( streamId: EventStreamId ) => Effect.Effect<Stream.Stream<V, never, never>, never, never>; readonly getHistorical: ( streamId: EventStreamId ) => Effect.Effect<Stream.Stream<V, never, never>, never, never>; readonly getAll: () => Effect.Effect<Stream.Stream<StreamEvent<V>, never, never>, never, never>; readonly getAllLiveOnly: () => Effect.Effect< Stream.Stream<StreamEvent<V>, never, never>, never, never >; } const createLiveEventStream = <V>(eventStream: EventStream<V>): Stream.Stream<V, never, never> => pipe(eventStream.events, Stream.fromChunk, Stream.concat(Stream.fromPubSub(eventStream.pubsub))); const createHistoricalEventStream = <V>( eventStream: EventStream<V> ): Stream.Stream<V, never, never> => pipe(eventStream.events, Stream.fromChunk); const findEventStreamOrDie = <V>( eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>, streamId: EventStreamId ): Effect.Effect<EventStream<V>, never, never> => pipe( eventStreamsById, HashMap.get(streamId), Option.match({ onNone: () => Effect.dieMessage( 'Event stream not found - this should not happen because we ensure it exists' ), onSome: Effect.succeed, }) ); const getEventStreamAndCreateLive = <V>( eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>, streamId: EventStreamId ): Effect.Effect<Stream.Stream<V, never, never>, never, never> => pipe(findEventStreamOrDie(eventStreamsById, streamId), Effect.map(createLiveEventStream)); const getEventStreamAndCreateHistorical = <V>( eventStreamsById: HashMap.HashMap<EventStreamId, EventStream<V>>, streamId: EventStreamId ): Effect.Effect<Stream.Stream<V, never, never>, never, never> => pipe(findEventStreamOrDie(eventStreamsById, streamId), Effect.map(createHistoricalEventStream)); const appendEventsAndUpdatePosition = <V>(streamEnd: EventStreamPosition, newEvents: Chunk.Chunk<V>) => (value: SynchronizedRef.SynchronizedRef<Value<V>>) => pipe( value, SynchronizedRef.updateEffect(appendToEventStream(streamEnd, newEvents)), Effect.as({ ...streamEnd, eventNumber: streamEnd.eventNumber + newEvents.length, }) ); const getLiveStreamForId = <V>(streamId: EventStreamId) => (value: SynchronizedRef.SynchronizedRef<Value<V>>) => pipe( value, SynchronizedRef.updateAndGetEffect(ensureEventStream(streamId)), Effect.flatMap(({ eventStreamsById }) => getEventStreamAndCreateLive(eventStreamsById, streamId) ) ); const getHistoricalStreamForId = <V>(streamId: EventStreamId) => (value: SynchronizedRef.SynchronizedRef<Value<V>>) => pipe( value, SynchronizedRef.updateAndGetEffect(ensureEventStream(streamId)), Effect.flatMap(({ eventStreamsById }) => getEventStreamAndCreateHistorical(eventStreamsById, streamId) ) ); const getAllEventsStream = <V>( value: SynchronizedRef.SynchronizedRef<Value<V>> ): Effect.Effect<Stream.Stream<StreamEvent<V>, never, never>, never, never> => pipe( value, SynchronizedRef.get, Effect.map(({ allEventsStream }) => createLiveEventStream(allEventsStream)) ); const getAllEventsLiveOnlyStream = <V>( value: SynchronizedRef.SynchronizedRef<Value<V>> ): Effect.Effect<Stream.Stream<StreamEvent<V>, never, never>, never, never> => pipe( value, SynchronizedRef.get, Effect.map(({ allEventsStream }) => Stream.fromPubSub(allEventsStream.pubsub)) ); const appendForStore = <V>(value: SynchronizedRef.SynchronizedRef<Value<V>>) => (streamEnd: EventStreamPosition) => (newEvents: Chunk.Chunk<V>) => pipe(value, appendEventsAndUpdatePosition(streamEnd, newEvents)); const getForStore = <V>(value: SynchronizedRef.SynchronizedRef<Value<V>>) => (streamId: EventStreamId) => pipe(value, getLiveStreamForId(streamId)); const getHistoricalForStore = <V>(value: SynchronizedRef.SynchronizedRef<Value<V>>) => (streamId: EventStreamId) => pipe(value, getHistoricalStreamForId(streamId)); export const make = <V>() => pipe( emptyStream<StreamEvent<V>>(), Effect.flatMap((allEventsStream: EventStream<StreamEvent<V>>) => SynchronizedRef.make<Value<V>>({ eventStreamsById: HashMap.empty<EventStreamId, EventStream<V>>(), allEventsStream, }) ), Effect.map( (value: SynchronizedRef.SynchronizedRef<Value<V>>): InMemoryStore<V> => ({ append: appendForStore(value), get: getForStore(value), getHistorical: getHistoricalForStore(value), getAll: () => getAllEventsStream(value), getAllLiveOnly: () => getAllEventsLiveOnlyStream(value), }) ) );