UNPKG

@codeforbreakfast/eventsourcing-store-inmemory

Version:

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

236 lines (204 loc) 7.86 kB
import { Duration, Effect, HashMap, Layer, Option, PubSub, Queue, Schedule, Scope, Stream, SynchronizedRef, pipe, } from 'effect'; import { EventStreamId, EventStoreError, EventStoreResourceError, eventStoreError, } from '@codeforbreakfast/eventsourcing-store'; interface SubscriptionData<T> { readonly pubsub: PubSub.PubSub<T>; readonly subscribers: number; } export interface InMemorySubscriptionManagerService { readonly subscribeToStream: ( streamId: EventStreamId ) => Effect.Effect<Stream.Stream<string, never>, EventStoreError, Scope.Scope>; readonly unsubscribeFromStream: ( streamId: EventStreamId ) => Effect.Effect<void, EventStoreError, never>; readonly getSubscriptionMetrics: () => Effect.Effect< { readonly activeStreams: number; readonly totalSubscribers: number; }, never, never >; } export class InMemorySubscriptionManager extends Effect.Tag('InMemorySubscriptionManager')< InMemorySubscriptionManager, InMemorySubscriptionManagerService >() {} const addSubscriptionDataToMap = <T>(streamId: EventStreamId, pubsub: PubSub.PubSub<T>) => (subs: HashMap.HashMap<EventStreamId, SubscriptionData<T>>) => HashMap.set(subs, streamId, { pubsub, subscribers: 0 }); const addPubsubToSubs = <T>(subs: HashMap.HashMap<EventStreamId, SubscriptionData<T>>, streamId: EventStreamId) => (pubsub: PubSub.PubSub<T>) => pipe(subs, addSubscriptionDataToMap(streamId, pubsub)); const createPubSubAndAddToMap = <T>( subs: HashMap.HashMap<EventStreamId, SubscriptionData<T>>, streamId: EventStreamId ) => pipe(512, PubSub.bounded<T>, Effect.map(addPubsubToSubs(subs, streamId)), Effect.runSync); const addSubscriptionIfMissing = <T>(streamId: EventStreamId) => (subs: HashMap.HashMap<EventStreamId, SubscriptionData<T>>) => pipe( subs, HashMap.get(streamId), Option.match({ onNone: () => createPubSubAndAddToMap(subs, streamId), onSome: () => subs, }) ); const extractSubscriptionData = <T>(streamId: EventStreamId) => (subscriptions: HashMap.HashMap<EventStreamId, SubscriptionData<T>>) => pipe( subscriptions, HashMap.get(streamId), Option.match({ onNone: () => Effect.fail( new EventStoreResourceError({ resource: `subscription for stream ${streamId}`, operation: 'create', cause: 'Failed to create subscription data', }) ), onSome: Effect.succeed, }) ); const getOrCreateSubscription = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, streamId: EventStreamId ): Effect.Effect<SubscriptionData<T>, EventStoreResourceError, never> => pipe( SynchronizedRef.updateAndGet(ref, addSubscriptionIfMissing(streamId)), Effect.flatMap(extractSubscriptionData(streamId)) ); const updateSubscribersCount = <T>(streamId: EventStreamId, delta: number) => (subscriptions: HashMap.HashMap<EventStreamId, SubscriptionData<T>>) => HashMap.modify(subscriptions, streamId, (data) => ({ ...data, subscribers: Math.max(0, data.subscribers + delta), })); const incrementSubscribers = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, streamId: EventStreamId ): Effect.Effect<void, never, never> => pipe(SynchronizedRef.update(ref, updateSubscribersCount(streamId, 1))); const decrementSubscribers = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, streamId: EventStreamId ): Effect.Effect<void, never, never> => pipe(SynchronizedRef.update(ref, updateSubscribersCount(streamId, -1))); const filterActiveSubscriptions = <T>( subscriptions: HashMap.HashMap<EventStreamId, SubscriptionData<T>> ) => HashMap.filter(subscriptions, (data) => data.subscribers > 0); const cleanupUnusedSubscriptions = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>> ): Effect.Effect<void, never, never> => pipe(SynchronizedRef.update(ref, filterActiveSubscriptions)); const createRetrySchedule = () => pipe( Schedule.exponential(Duration.millis(100), 1.5), Schedule.whileOutput((d) => Duration.toMillis(d) < 30000) ); const createStreamFromQueue = <T>(queue: Queue.Dequeue<T>) => pipe( Stream.fromQueue(queue, { shutdown: true }), Stream.map(String), Stream.retry(createRetrySchedule()) ); const decrementAndCleanup = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, streamId: EventStreamId ) => pipe( decrementSubscribers(ref, streamId), Effect.tap(() => cleanupUnusedSubscriptions(ref)) ); const subscribeToQueue = <T>(subData: SubscriptionData<T>) => pipe(subData.pubsub, PubSub.subscribe, Effect.map(createStreamFromQueue)); const createStreamWithCleanup = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, streamId: EventStreamId ) => (subData: SubscriptionData<T>) => pipe(subData, subscribeToQueue, Effect.ensuring(decrementAndCleanup(ref, streamId))); const createSubscribeError = (streamId: EventStreamId) => eventStoreError.subscribe(streamId, `Failed to subscribe to stream: ${String(streamId)}`); const createUnsubscribeError = (streamId: EventStreamId) => eventStoreError.subscribe(streamId, `Failed to unsubscribe from stream: ${String(streamId)}`); const subscribeToStreamEffect = <T>(streamId: EventStreamId) => (ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>) => pipe( getOrCreateSubscription(ref, streamId), Effect.tap(() => incrementSubscribers(ref, streamId)), Effect.flatMap(createStreamWithCleanup(ref, streamId)), Effect.mapError(createSubscribeError(streamId)) ); const unsubscribeFromStreamEffect = <T>(streamId: EventStreamId) => (ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>) => pipe(decrementAndCleanup(ref, streamId), Effect.mapError(createUnsubscribeError(streamId))); const calculateTotalSubscribers = <T>( subscriptions: HashMap.HashMap<EventStreamId, SubscriptionData<T>> ) => pipe(subscriptions, HashMap.values, (values) => Array.from(values).reduce((sum, data) => sum + data.subscribers, 0) ); const getMetricsEffect = <T>( ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>> ) => pipe( ref, SynchronizedRef.get, Effect.map((subscriptions) => { const activeStreams = HashMap.size(subscriptions); const totalSubscribers = calculateTotalSubscribers(subscriptions); return { activeStreams, totalSubscribers }; }) ); const subscribeForManager = <T>(ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>) => (streamId: EventStreamId) => pipe(ref, subscribeToStreamEffect(streamId)); const unsubscribeForManager = <T>(ref: SynchronizedRef.SynchronizedRef<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>) => (streamId: EventStreamId) => pipe(ref, unsubscribeFromStreamEffect(streamId)); export const makeInMemorySubscriptionManager = <T>(): Effect.Effect< InMemorySubscriptionManagerService, never, never > => pipe( HashMap.empty(), SynchronizedRef.make<HashMap.HashMap<EventStreamId, SubscriptionData<T>>>, Effect.map((ref) => ({ subscribeToStream: subscribeForManager(ref), unsubscribeFromStream: unsubscribeForManager(ref), getSubscriptionMetrics: () => getMetricsEffect(ref), })) ); export const InMemorySubscriptionManagerLive = <T>() => Layer.effect(InMemorySubscriptionManager, makeInMemorySubscriptionManager<T>());