145 lines
4.0 KiB
TypeScript
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,
|
|
}));
|