@codeforbreakfast/eventsourcing-store-postgres
Version:
Production-ready PostgreSQL event store with Effect integration - Scalable, ACID-compliant event persistence with type-safe database operations and streaming
117 lines (106 loc) • 3.7 kB
text/typescript
import { PgClient } from '@effect/sql-pg';
import { Duration, Effect, Layer, Schedule, pipe } from 'effect';
import { EventStoreConnectionError, connectionError } from '@codeforbreakfast/eventsourcing-store';
// Extended PgClient type with direct query access
interface PgClientWithQuery extends PgClient.PgClient {
query: (sql: string) => Promise<unknown>;
end: () => Promise<void>;
}
/**
* ConnectionManager service for managing PostgreSQL notification connections
*/
interface ConnectionManagerService {
/**
* Get dedicated connection for LISTEN operations
*/
getListenConnection: Effect.Effect<PgClient.PgClient, EventStoreConnectionError, never>;
/**
* Execute health check on listening connection
*/
healthCheck: Effect.Effect<void, EventStoreConnectionError, never>;
/**
* Gracefully close the listen connection
*/
shutdown: Effect.Effect<void, EventStoreConnectionError, never>;
}
export class ConnectionManager extends Effect.Tag('ConnectionManager')<
ConnectionManager,
ConnectionManagerService
>() {}
/**
* Create a robust persistent connection specifically for LISTEN/NOTIFY operations
* This uses the main PgClient from the layer, so it inherits the same configuration
*/
const createListenConnection = pipe(
PgClient.PgClient,
// Add debug logging
Effect.tap(() => Effect.logDebug('PostgreSQL notification listener connection established')),
Effect.tapError((error) =>
Effect.logError('Failed to establish notification listener connection', {
error,
})
),
Effect.mapError((error) =>
connectionError.retryable('establish notification listener connection', error)
)
);
/**
* Implementation of ConnectionManager service
*/
export const ConnectionManagerLive = Layer.effect(
ConnectionManager,
pipe(
createListenConnection,
Effect.map((listenConnection) => ({
getListenConnection: Effect.succeed(listenConnection),
healthCheck: pipe(
Effect.succeed(listenConnection),
Effect.flatMap((client) =>
Effect.tryPromise({
try: () => (client as PgClientWithQuery).query('SELECT 1 AS health_check'),
catch: (error) => error,
})
),
Effect.tap(() => Effect.logDebug('PostgreSQL notification listener health check passed')),
Effect.tapError((error) =>
Effect.logError('PostgreSQL notification listener health check failed', { error })
),
Effect.mapError((error) =>
connectionError.retryable('health check notification listener', error)
),
Effect.as(undefined)
),
shutdown: pipe(
Effect.succeed(listenConnection),
Effect.flatMap((client) =>
Effect.tryPromise({
try: () => (client as PgClientWithQuery).end(),
catch: (error) => error,
})
),
Effect.tap(() => Effect.logInfo('PostgreSQL notification listener connection closed')),
Effect.tapError((error) =>
Effect.logError('Failed to close PostgreSQL notification listener connection', { error })
),
Effect.mapError((error) => connectionError.fatal('shutdown notification listener', error)),
Effect.as(undefined)
),
}))
)
);
/**
* Wraps an effect with automatic health checks and retry policy
*/
// Skip health check as it's causing issues
export const withConnectionHealth = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
pipe(
effect,
Effect.retry(
pipe(
Schedule.exponential(Duration.millis(100), 1.5),
Schedule.whileOutput((d) => Duration.toMillis(d) < 60000) // Max 1 minute delay
)
)
);