Files
music-repo/packages/frontend/src/cache.ts
2024-12-26 21:40:05 +01:00

145 lines
4.0 KiB
TypeScript

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<User>;
readonly createdAt: string;
readonly modifiedBy: Option.Option<User>;
readonly modifiedAt: Option.Option<string>;
}
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<string>;
readonly lyricist: Option.Option<string>;
readonly arranger: Option.Option<string>;
readonly attachments: readonly Attachment[];
}
interface DbSystemInformation {
readonly createdBy: UserId | null;
readonly createdAt: string;
readonly modifiedBy: UserId | null;
readonly modifiedAt: string | null;
}
export const denormalizeSystemInformation = <T extends DbSystemInformation>({
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<Db.Attachment, "sha256"> & { 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,
}));