import type * as Db from "backend/database"; import { AttachmentId, PieceId, RepertoireId, UserId } from "common"; import { Cache, Duration, Effect, Option, pipe } from "effect"; import { client, mapResponse } from "./client"; export interface User { readonly userId: UserId; readonly username: string; readonly admin: boolean; } export interface SystemInformation { readonly createdBy: Option.Option; readonly createdAt: string; readonly modifiedBy: Option.Option; readonly modifiedAt: Option.Option; } export interface Attachment extends SystemInformation { readonly attachmentId: AttachmentId; readonly pieceId: PieceId; readonly sha256: string; readonly filename: string; readonly mediaType: string; } export interface Piece extends SystemInformation { readonly pieceId: PieceId; readonly name: string; readonly composer: Option.Option; readonly lyricist: Option.Option; readonly arranger: Option.Option; readonly attachments: readonly Attachment[]; } interface DbSystemInformation { readonly createdBy: UserId | null; readonly createdAt: string; readonly modifiedBy: UserId | null; readonly modifiedAt: string | null; } export const denormalizeSystemInformation = ({ createdBy, modifiedBy, modifiedAt, ...rest }: T) => pipe( Effect.all({ createdBy: pipe( createdBy, Effect.fromNullable, Effect.flatMap((userId) => Effect.uninterruptible(userCache.get(userId))), Effect.optionFromOptional, ), modifiedBy: pipe( modifiedBy, Effect.fromNullable, Effect.flatMap((userId) => Effect.uninterruptible(userCache.get(userId))), Effect.optionFromOptional, ), }, { concurrency: "unbounded" }), Effect.map((si) => Object.freeze({ ...rest, ...si, modifiedAt: Option.fromNullable(modifiedAt), })), ); export const denormalizePiece = ({ composer, lyricist, arranger, attachments, ...rest }: Db.Piece & { attachments: (Omit & { sha256: string })[] }) => pipe( Effect.all({ attachments: Effect.all(attachments.map(denormalizeSystemInformation), { concurrency: "unbounded" }), }, { concurrency: "unbounded" }), Effect.map((piece) => Object.freeze({ ...rest, ...piece, composer: Option.fromNullable(composer), lyricist: Option.fromNullable(lyricist), arranger: Option.fromNullable(arranger), })), Effect.flatMap(denormalizeSystemInformation), ); export const denormalizeRepertoire = ({ entries, ...rest }: Db.Repertoire & { entries: PieceId[] }) => pipe( Effect.all({ entries: Effect.all(entries.map((entry) => Effect.uninterruptible(pieceCache.get(entry))), { concurrency: "unbounded" }), }, { concurrency: "unbounded" }), Effect.map((repertoire) => Object.freeze({ ...rest, ...repertoire, })), Effect.flatMap(denormalizeSystemInformation), ); const UserSemaphore = Effect.unsafeMakeSemaphore(1); const CacheSemaphore = Effect.unsafeMakeSemaphore(4); const RepertoireSemaphore = Effect.unsafeMakeSemaphore(1); export const userLookup = (userId: UserId) => pipe( Effect.promise((signal) => client.user({ userId }).get({ fetch: { signal } })), Effect.flatMap(mapResponse), UserSemaphore.withPermits(1), ); export const pieceLookup = (pieceId: PieceId) => pipe( Effect.promise((signal) => client.piece({ pieceId }).get({ fetch: { signal } })), Effect.flatMap(mapResponse), Effect.flatMap(denormalizePiece), CacheSemaphore.withPermits(1), ); export const repertoireLookup = (repertoireId: RepertoireId) => pipe( Effect.promise((signal) => client.repertoire({ repertoireId }).get({ fetch: { signal } })), Effect.flatMap(mapResponse), Effect.flatMap(denormalizeRepertoire), RepertoireSemaphore.withPermits(1), ); export const userCache = Effect.runSync(Cache.make({ capacity: Infinity, timeToLive: Duration.days(1), lookup: userLookup, })); export const pieceCache = Effect.runSync(Cache.make({ capacity: Infinity, timeToLive: Duration.days(1), lookup: pieceLookup, })); export const repertoireCache = Effect.runSync(Cache.make({ capacity: Infinity, timeToLive: Duration.days(1), lookup: repertoireLookup, }));