import { Chunk, Effect, HashMap, Option, PubSub, Stream, SynchronizedRef, pipe } from 'effect'; import { EventStreamId, EventStreamPosition, ConcurrencyConflictError, type StreamEvent, } from '@codeforbreakfast/eventsourcing-store'; interface EventStream { readonly events: Chunk.Chunk; readonly pubsub: PubSub.PubSub; } const emptyStream = (): Effect.Effect, never, never> => pipe( 2 ^ 8, PubSub.bounded, Effect.map((pubsub) => ({ events: Chunk.empty(), pubsub, })) ); const createUpdatedEventStream = ( eventStream: EventStream, newEvents: Chunk.Chunk ): EventStream => ({ events: pipe(eventStream.events, Chunk.appendAll(newEvents)), pubsub: eventStream.pubsub, }); const appendToExistingEventStream = (position: EventStreamPosition, newEvents: Chunk.Chunk) => (eventStream: EventStream) => 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 = (streamEnd: EventStreamPosition, newEvents: Chunk.Chunk) => pipe(emptyStream(), Effect.flatMap(appendToExistingEventStream(streamEnd, newEvents))); const logAppendedEvents = (newEvents: Chunk.Chunk, streamEnd: EventStreamPosition) => Effect.annotateLogs(Effect.logInfo(`Appended events to stream`), { newEvents, streamEnd, }); const publishEventsToStream = (pubsub: PubSub.PubSub, newEvents: Chunk.Chunk) => pipe(pubsub, PubSub.publishAll(newEvents)); const updateEventStreamsById = ( updatedEventStream: EventStream, streamEnd: EventStreamPosition, newEvents: Chunk.Chunk ) => (eventStreamsById: HashMap.HashMap>) => pipe( eventStreamsById, HashMap.set(streamEnd.streamId, updatedEventStream), Effect.succeed, Effect.tap(() => logAppendedEvents(newEvents, streamEnd)), Effect.tap(() => publishEventsToStream(updatedEventStream.pubsub, newEvents)) ); const tagEventsWithStreamId = ( newEvents: Chunk.Chunk, streamId: EventStreamId, startingEventNumber: number ) => pipe( newEvents, Chunk.map((event, index) => ({ position: { streamId, eventNumber: startingEventNumber + index }, event, })) ); const publishToAllEventsStream = ( allEventsStream: EventStream>, taggedEvents: Chunk.Chunk> ) => publishEventsToStream(allEventsStream.pubsub, taggedEvents); const createUpdatedValue = ( allEventsStream: EventStream>, newEvents: Chunk.Chunk, streamEnd: EventStreamPosition ) => (eventStreamsById: HashMap.HashMap>): Value => { const taggedEvents = tagEventsWithStreamId( newEvents, streamEnd.streamId, streamEnd.eventNumber ); return { eventStreamsById, allEventsStream: { events: pipe(allEventsStream.events, Chunk.appendAll(taggedEvents)), pubsub: allEventsStream.pubsub, }, }; }; const applyUpdatedEventStreamToValue = ( allEventsStream: EventStream>, streamEnd: EventStreamPosition, newEvents: Chunk.Chunk, eventStreamsById: HashMap.HashMap> ) => (updatedEventStream: EventStream) => 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 = (streamEnd: EventStreamPosition, newEvents: Chunk.Chunk) => ({ eventStreamsById, allEventsStream, }: Value): Effect.Effect, ConcurrencyConflictError, never> => pipe( eventStreamsById, HashMap.get(streamEnd.streamId), Option.match({ onSome: appendToExistingEventStream(streamEnd, newEvents), onNone: () => createOrAppendToStream(streamEnd, newEvents), }), Effect.flatMap( applyUpdatedEventStreamToValue(allEventsStream, streamEnd, newEvents, eventStreamsById) ) ); const modifyEventStreamsWithEmptyIfMissing = (streamId: EventStreamId, emptyStream: EventStream) => (eventStreamsById: HashMap.HashMap>) => HashMap.modifyAt( eventStreamsById, streamId, Option.match({ onNone: () => Option.some(emptyStream), onSome: Option.some, }) ); const modifyEventStreamsByIdWithEmptyStream = ( streamId: EventStreamId, emptyStream: EventStream, eventStreamsById: HashMap.HashMap> ): Effect.Effect>, never, never> => pipe( eventStreamsById, modifyEventStreamsWithEmptyIfMissing(streamId, emptyStream), Effect.succeed ); const buildValueFromEventStreamsById = (allEventsStream: EventStream>) => (eventStreamsById: HashMap.HashMap>): Value => ({ eventStreamsById, allEventsStream, }); const ensureEventStream = (streamId: EventStreamId) => ({ eventStreamsById, allEventsStream }: Value): Effect.Effect, never, never> => pipe( emptyStream(), Effect.flatMap((emptyStream) => modifyEventStreamsByIdWithEmptyStream(streamId, emptyStream, eventStreamsById) ), Effect.map(buildValueFromEventStreamsById(allEventsStream)) ); interface Value { readonly eventStreamsById: HashMap.HashMap>; readonly allEventsStream: EventStream>; } export interface InMemoryStore { readonly append: ( to: EventStreamPosition ) => ( events: Chunk.Chunk ) => Effect.Effect; readonly get: ( streamId: EventStreamId ) => Effect.Effect, never, never>; readonly getHistorical: ( streamId: EventStreamId ) => Effect.Effect, never, never>; readonly getAll: () => Effect.Effect, never, never>, never, never>; readonly getAllLiveOnly: () => Effect.Effect< Stream.Stream, never, never>, never, never >; } const createLiveEventStream = (eventStream: EventStream): Stream.Stream => pipe(eventStream.events, Stream.fromChunk, Stream.concat(Stream.fromPubSub(eventStream.pubsub))); const createHistoricalEventStream = ( eventStream: EventStream ): Stream.Stream => pipe(eventStream.events, Stream.fromChunk); const findEventStreamOrDie = ( eventStreamsById: HashMap.HashMap>, streamId: EventStreamId ): Effect.Effect, 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 = ( eventStreamsById: HashMap.HashMap>, streamId: EventStreamId ): Effect.Effect, never, never> => pipe(findEventStreamOrDie(eventStreamsById, streamId), Effect.map(createLiveEventStream)); const getEventStreamAndCreateHistorical = ( eventStreamsById: HashMap.HashMap>, streamId: EventStreamId ): Effect.Effect, never, never> => pipe(findEventStreamOrDie(eventStreamsById, streamId), Effect.map(createHistoricalEventStream)); const appendEventsAndUpdatePosition = (streamEnd: EventStreamPosition, newEvents: Chunk.Chunk) => (value: SynchronizedRef.SynchronizedRef>) => pipe( value, SynchronizedRef.updateEffect(appendToEventStream(streamEnd, newEvents)), Effect.as({ ...streamEnd, eventNumber: streamEnd.eventNumber + newEvents.length, }) ); const getLiveStreamForId = (streamId: EventStreamId) => (value: SynchronizedRef.SynchronizedRef>) => pipe( value, SynchronizedRef.updateAndGetEffect(ensureEventStream(streamId)), Effect.flatMap(({ eventStreamsById }) => getEventStreamAndCreateLive(eventStreamsById, streamId) ) ); const getHistoricalStreamForId = (streamId: EventStreamId) => (value: SynchronizedRef.SynchronizedRef>) => pipe( value, SynchronizedRef.updateAndGetEffect(ensureEventStream(streamId)), Effect.flatMap(({ eventStreamsById }) => getEventStreamAndCreateHistorical(eventStreamsById, streamId) ) ); const getAllEventsStream = ( value: SynchronizedRef.SynchronizedRef> ): Effect.Effect, never, never>, never, never> => pipe( value, SynchronizedRef.get, Effect.map(({ allEventsStream }) => createLiveEventStream(allEventsStream)) ); const getAllEventsLiveOnlyStream = ( value: SynchronizedRef.SynchronizedRef> ): Effect.Effect, never, never>, never, never> => pipe( value, SynchronizedRef.get, Effect.map(({ allEventsStream }) => Stream.fromPubSub(allEventsStream.pubsub)) ); const appendForStore = (value: SynchronizedRef.SynchronizedRef>) => (streamEnd: EventStreamPosition) => (newEvents: Chunk.Chunk) => pipe(value, appendEventsAndUpdatePosition(streamEnd, newEvents)); const getForStore = (value: SynchronizedRef.SynchronizedRef>) => (streamId: EventStreamId) => pipe(value, getLiveStreamForId(streamId)); const getHistoricalForStore = (value: SynchronizedRef.SynchronizedRef>) => (streamId: EventStreamId) => pipe(value, getHistoricalStreamForId(streamId)); export const make = () => pipe( emptyStream>(), Effect.flatMap((allEventsStream: EventStream>) => SynchronizedRef.make>({ eventStreamsById: HashMap.empty>(), allEventsStream, }) ), Effect.map( (value: SynchronizedRef.SynchronizedRef>): InMemoryStore => ({ append: appendForStore(value), get: getForStore(value), getHistorical: getHistoricalForStore(value), getAll: () => getAllEventsStream(value), getAllLiveOnly: () => getAllEventsLiveOnlyStream(value), }) ) );