import { ProviderSessionId, ThreadId } from "@t3tools/contracts"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; import { Effect, Layer, Option, Schema } from "effect"; import { ProviderSessionRepositoryPersistenceError, ProviderSessionRepositoryValidationError, } from "../Errors.ts"; import { ProviderSessionRepository, type ProviderSessionEntry, type ProviderSessionRepositoryShape, } from "../Services/ProviderSessions.ts"; const ProviderKind = Schema.Literals(["codex", "claudeCode"]); const ProviderSessionRowSchema = Schema.Struct({ sessionId: ProviderSessionId, provider: ProviderKind, threadId: Schema.NullOr(ThreadId), createdAt: Schema.String, updatedAt: Schema.String, }); const SessionIdRequestSchema = Schema.Struct({ sessionId: ProviderSessionId, }); const UpsertSessionRequestSchema = Schema.Struct({ sessionId: ProviderSessionId, provider: ProviderKind, threadId: Schema.NullOr(ThreadId), }); function errorMessage(cause: unknown, fallback: string): string { if (cause instanceof Error && cause.message.length > 0) { return cause.message; } return fallback; } function toValidationError( operation: string, cause: unknown, ): ProviderSessionRepositoryValidationError { return new ProviderSessionRepositoryValidationError({ operation, issue: errorMessage(cause, "Invalid provider session repository input."), cause, }); } function decodeInput( schema: Schema.Schema, input: unknown, operation: string, ): Effect.Effect { return Schema.decodeUnknownEffect(schema)(input).pipe( Effect.mapError((cause) => toValidationError(operation, cause)), ) as Effect.Effect; } function toPersistenceError( operation: string, cause: unknown, ): ProviderSessionRepositoryPersistenceError { return new ProviderSessionRepositoryPersistenceError({ operation, detail: `Failed to execute ${operation}.`, cause, }); } function toEntry(row: Schema.Schema.Type): ProviderSessionEntry { return { sessionId: row.sessionId, provider: row.provider, ...(row.threadId !== null ? { threadId: row.threadId } : {}), createdAt: row.createdAt, updatedAt: row.updatedAt, }; } const makeProviderSessionRepository = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const upsertSessionRow = SqlSchema.findOne({ Request: UpsertSessionRequestSchema, Result: ProviderSessionRowSchema, execute: (request) => sql` INSERT INTO provider_sessions ( session_id, provider, thread_id, created_at, updated_at ) VALUES ( ${request.sessionId}, ${request.provider}, ${request.threadId}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ) ON CONFLICT (session_id) DO UPDATE SET provider = excluded.provider, thread_id = excluded.thread_id, updated_at = CURRENT_TIMESTAMP RETURNING session_id AS "sessionId", provider, thread_id AS "threadId", created_at AS "createdAt", updated_at AS "updatedAt" `, }); const findSessionRow = SqlSchema.findOneOption({ Request: SessionIdRequestSchema, Result: ProviderSessionRowSchema, execute: ({ sessionId }) => sql` SELECT session_id AS "sessionId", provider, thread_id AS "threadId", created_at AS "createdAt", updated_at AS "updatedAt" FROM provider_sessions WHERE session_id = ${sessionId} `, }); const listSessionRows = SqlSchema.findAll({ Request: Schema.Void, Result: ProviderSessionRowSchema, execute: () => sql` SELECT session_id AS "sessionId", provider, thread_id AS "threadId", created_at AS "createdAt", updated_at AS "updatedAt" FROM provider_sessions ORDER BY created_at ASC, session_id ASC `, }); const deleteSessionRow = SqlSchema.void({ Request: SessionIdRequestSchema, execute: ({ sessionId }) => sql`DELETE FROM provider_sessions WHERE session_id = ${sessionId}`, }); const upsertSession: ProviderSessionRepositoryShape["upsertSession"] = (input) => Effect.gen(function* () { const parsed = yield* decodeInput( UpsertSessionRequestSchema, { sessionId: input.sessionId, provider: input.provider, threadId: input.threadId ?? null, }, "ProviderSessionRepository.upsertSession", ); yield* upsertSessionRow(parsed).pipe( Effect.mapError((cause) => toPersistenceError("ProviderSessionRepository.upsertSession:query", cause), ), Effect.asVoid, ); }); const getSession: ProviderSessionRepositoryShape["getSession"] = (input) => Effect.gen(function* () { const parsed = yield* decodeInput( SessionIdRequestSchema, { sessionId: input.sessionId }, "ProviderSessionRepository.getSession", ); const row = yield* findSessionRow(parsed).pipe( Effect.mapError((cause) => toPersistenceError("ProviderSessionRepository.getSession:query", cause), ), ); return Option.map(row, toEntry); }); const listSessions: ProviderSessionRepositoryShape["listSessions"] = () => listSessionRows(undefined).pipe( Effect.mapError((cause) => toPersistenceError("ProviderSessionRepository.listSessions:query", cause), ), Effect.map((rows) => rows.map(toEntry)), ); const deleteSession: ProviderSessionRepositoryShape["deleteSession"] = (input) => Effect.gen(function* () { const parsed = yield* decodeInput( SessionIdRequestSchema, { sessionId: input.sessionId }, "ProviderSessionRepository.deleteSession", ); yield* deleteSessionRow(parsed).pipe( Effect.mapError((cause) => toPersistenceError("ProviderSessionRepository.deleteSession:query", cause), ), ); }); return { upsertSession, getSession, listSessions, deleteSession, } satisfies ProviderSessionRepositoryShape; }); export const ProviderSessionRepositoryLive = Layer.effect( ProviderSessionRepository, makeProviderSessionRepository, );