import { Duration, Effect, HashMap, Layer, Option, PubSub, Schedule, Stream, SynchronizedRef, pipe, } from 'effect'; import type { ReadonlyDeep } from 'type-fest'; import { EventStreamId, EventStreamPosition, EventStoreError, eventStoreError, type StreamEvent, } from '@codeforbreakfast/eventsourcing-store'; /** * Container for subscription data for a specific stream */ interface SubscriptionData { readonly pubsub: PubSub.PubSub; } /** * State for the subscription manager */ interface SubscriptionManagerState { readonly streams: HashMap.HashMap>; readonly allEventsPubSub: PubSub.PubSub>; } /** * SubscriptionManager service for managing subscriptions to event streams */ export interface SubscriptionManagerService { /** * Subscribe to a specific event stream and return a Stream of events */ readonly subscribeToStream: ( streamId: EventStreamId ) => Effect.Effect, EventStoreError, never>; /** * Unsubscribe from a specific event stream */ readonly unsubscribeFromStream: ( streamId: EventStreamId ) => Effect.Effect; /** * Publish an event to all subscribers of a stream */ readonly publishEvent: ( streamId: EventStreamId, event: string ) => Effect.Effect; /** * Subscribe to all events from all streams */ readonly subscribeToAllEvents: () => Effect.Effect< Stream.Stream, never>, EventStoreError, never >; /** * Publish an event to all-events subscribers */ readonly publishToAllEvents: ( position: EventStreamPosition, event: string ) => Effect.Effect; } export class SubscriptionManager extends Effect.Tag('SubscriptionManager')< SubscriptionManager, SubscriptionManagerService >() {} const updateHashMapWithPubSub = (streamId: EventStreamId, subs: HashMap.HashMap>) => (pubsub: PubSub.PubSub) => { const data = { pubsub }; return HashMap.set(subs, streamId, data); }; const createPubSubAndUpdateHashMap = ( streamId: EventStreamId, subs: HashMap.HashMap> ): HashMap.HashMap> => pipe(256, PubSub.bounded, Effect.map(updateHashMapWithPubSub(streamId, subs)), Effect.runSync); const getOrCreateSubscription = (streamId: EventStreamId) => ( subs: HashMap.HashMap> ): HashMap.HashMap> => pipe( subs, HashMap.get(streamId), Option.match({ onNone: () => createPubSubAndUpdateHashMap(streamId, subs), onSome: () => subs, }) ); const extractSubscriptionData = (streamId: EventStreamId) => ( subscriptions: HashMap.HashMap> ): Effect.Effect, never, never> => pipe( subscriptions, HashMap.get(streamId), Option.match({ onNone: () => Effect.die("Subscription should exist but doesn't"), onSome: Effect.succeed, }) ); const updateStateWithNewSubscription = (streamId: EventStreamId) => (state: SubscriptionManagerState): SubscriptionManagerState => pipe(state.streams, getOrCreateSubscription(streamId), (streams) => ({ ...state, streams, })); /** * Get or create a PubSub for a stream ID */ const getOrCreatePubSub = ( ref: ReadonlyDeep>, streamId: EventStreamId ): Effect.Effect, never, never> => pipe( ref, SynchronizedRef.updateAndGet(updateStateWithNewSubscription(streamId)), Effect.map((state) => state.streams), Effect.flatMap(extractSubscriptionData(streamId)) ); const updateStateByRemovingStream = (streamId: EventStreamId) => (state: SubscriptionManagerState): SubscriptionManagerState => ({ ...state, streams: HashMap.remove(state.streams, streamId), }); /** * Remove a subscription for a stream ID */ const removeSubscription = ( ref: ReadonlyDeep>, streamId: EventStreamId ): Effect.Effect => pipe(ref, SynchronizedRef.update(updateStateByRemovingStream(streamId)), Effect.as(undefined)); const publishEventToPubSub = (event: string, streamId: EventStreamId) => (subData: ReadonlyDeep>): Effect.Effect => pipe( subData.pubsub, PubSub.publish(event), Effect.tapError((error) => Effect.logError('Failed to publish event to subscribers', { error, streamId, }) ) ); const publishToSubscriptionIfExists = (streamId: EventStreamId, event: string) => ( subscriptions: HashMap.HashMap> ): Effect.Effect => pipe( subscriptions, HashMap.get(streamId), Option.match({ onNone: () => Effect.succeed(undefined), onSome: publishEventToPubSub(event, streamId), }) ); /** * Publish an event to subscribers of a stream */ const publishToStream = ( ref: ReadonlyDeep>, streamId: EventStreamId, event: string ): Effect.Effect => pipe( ref, SynchronizedRef.get, Effect.map((state) => state.streams), Effect.flatMap(publishToSubscriptionIfExists(streamId, event)) ); const createRetrySchedule = (): Schedule.Schedule => pipe( Schedule.exponential(Duration.millis(100), 1.5), Schedule.whileOutput((d) => Duration.toMillis(d) < 30000) ); const createStreamFromPubSub = ( pubsub: ReadonlyDeep> ): Stream.Stream => pipe( pubsub.pubsub, (p) => Stream.fromPubSub(p as PubSub.PubSub), Stream.retry(createRetrySchedule()) ); const createSubscriptionStream = (ref: ReadonlyDeep>) => (streamId: EventStreamId): Effect.Effect, EventStoreError, never> => pipe( getOrCreatePubSub(ref, streamId), Effect.map(createStreamFromPubSub), Effect.mapError(eventStoreError.subscribe(streamId, 'Failed to subscribe to stream')) ); const unsubscribeFromStreamWithErrorHandling = (ref: ReadonlyDeep>) => (streamId: EventStreamId): Effect.Effect => pipe( removeSubscription(ref, streamId), Effect.mapError(eventStoreError.subscribe(streamId, 'Failed to unsubscribe from stream')) ); const publishEventWithErrorHandling = (ref: ReadonlyDeep>) => (streamId: EventStreamId, event: string): Effect.Effect => pipe( publishToStream(ref, streamId, event), Effect.mapError(eventStoreError.write(streamId, 'Failed to publish event to subscribers')) ); const createAllEventsStreamFromPubSub = ( pubsub: PubSub.PubSub> ): Stream.Stream, never> => Stream.retry(Stream.fromPubSub(pubsub), createRetrySchedule()); const subscribeToAllEventsStream = (ref: ReadonlyDeep>) => (): Effect.Effect, never>, EventStoreError, never> => pipe( ref, SynchronizedRef.get, Effect.map((state) => createAllEventsStreamFromPubSub(state.allEventsPubSub)), Effect.mapError(eventStoreError.subscribe('*', 'Failed to subscribe to all events')) ); const publishToAllEventsPubSub = (position: EventStreamPosition, event: string) => (pubsub: PubSub.PubSub>): Effect.Effect => pipe( pubsub, PubSub.publish({ position, event }), Effect.tapError((error) => Effect.logError('Failed to publish to all-events', { error, streamId: position.streamId }) ) ); const publishEventToAllEventsPubSub = (position: EventStreamPosition, event: string) => (state: SubscriptionManagerState): Effect.Effect => pipe(state.allEventsPubSub, publishToAllEventsPubSub(position, event)); const publishToAllEventsWithErrorHandling = (ref: ReadonlyDeep>) => (position: EventStreamPosition, event: string): Effect.Effect => pipe( ref, SynchronizedRef.get, Effect.flatMap(publishEventToAllEventsPubSub(position, event)), Effect.mapError( eventStoreError.write(position.streamId, 'Failed to publish to all-events subscribers') ) ); const createSubscriptionManagerService = ( ref: SynchronizedRef.SynchronizedRef ): SubscriptionManagerService => ({ subscribeToStream: createSubscriptionStream(ref), unsubscribeFromStream: unsubscribeFromStreamWithErrorHandling(ref), publishEvent: publishEventWithErrorHandling(ref), subscribeToAllEvents: subscribeToAllEventsStream(ref), publishToAllEvents: publishToAllEventsWithErrorHandling(ref), }); const makeSubscriptionManagerState = ( allEventsPubSub: PubSub.PubSub> ): SubscriptionManagerState => ({ streams: HashMap.empty(), allEventsPubSub, }); const createManagerFromState = ( allEventsPubSub: PubSub.PubSub> ): Effect.Effect => pipe( allEventsPubSub, makeSubscriptionManagerState, SynchronizedRef.make, Effect.map(createSubscriptionManagerService) ); /** * Implementation of SubscriptionManager service */ export const SubscriptionManagerLive = Layer.effect( SubscriptionManager, pipe(PubSub.unbounded>(), Effect.flatMap(createManagerFromState)) );