import crypto from 'crypto' import { readFile, writeFile, access, constants } from 'fs/promises' import { MetadataProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User } from '../../shared/model' function once any>(fn: T, message: string): T { let flag = false return ((...args: any) => { if (flag) { throw new Error(message ?? `cannot run more than once`) } flag = true return fn(...args) }) as T } type Lock = () => void type Mutex = { lock(): Promise } function createMutex(): Mutex { let locked = false const waiters: ((fn: () => void) => void)[] = [] const unlock = () => { if (waiters.length > 0) { console.log(`[Database/Mutex] Passing lock to next in queue (of size ${waiters.length})`) const resolve = waiters.shift()!! resolve(once(unlock, `lock already released`)) } else { locked = false console.log(`[Database/Mutex] Releasing the lock`) } } const lock = (): Promise => { if (locked) { console.log(`[Database/Mutex] Putting into queue`) return new Promise(resolve => { waiters.push(resolve) }) } else { console.log(`[Database/Mutex] Acquiring the lock`) locked = true return Promise.resolve(once(unlock, `lock already released`)) } } return { lock } } type DatabaseConnection = { path: string initialValue: Database mu: Mutex } export type Database = { users: Record problems: Record solutions: Record } export function createDatabase(path: string, initialValue: Database) { return { path, initialValue, mu: createMutex(), } } async function withDatabase({ path, initialValue, mu }: DatabaseConnection, fn: (db: Database) => R | Promise): Promise { const unlock: Lock = await mu.lock() try { await access(path, constants.R_OK) } catch (e) { console.log(`[Database] Creating empty database into "${path}"`) await writeFile(path, JSON.stringify(initialValue, null, 4)) } console.log(`[Database] Loading database from "${path}"`) const state = JSON.parse(await readFile(path, 'utf-8')) const result = await fn(state) console.log(`[Database] Saving database to "${path}"`) await writeFile(path, JSON.stringify(state, null, 4)) unlock() return result } // // Users // export const getUsers = (db: DatabaseConnection) => withDatabase(db, state => { return Object.values(state.users) }) export const getUser: (db: DatabaseConnection, id: string) => Promise = (db, id) => withDatabase(db, state => { return state.users[id] ?? null }) // // Problems // export const createProblem = (db: DatabaseConnection, { title, content, createdBy }: Omit): Promise => withDatabase(db, state => { const problemIds = Object.keys(state.problems) const nextId = (problemIds.length > 0 ? String(parseInt(problemIds.at(-1)!) + 1) : '1') as ProblemId state.problems[nextId] = { id: nextId, createdAt: new Date().toJSON(), deleted: false, title, content, createdBy, } return nextId }) export const getProblem = (db: DatabaseConnection, id: string): Promise => withDatabase(db, state => { const problem = (state.problems[id] ?? null) as Problem | null return problem && !problem.deleted ? problem : null }) export const getProblems = (db: DatabaseConnection): Promise => withDatabase(db, state => { return Object.values(state.problems).filter(p => !p.deleted) }) export const updateProblem = (db: DatabaseConnection, id: ProblemId, problem: Partial>): Promise => withDatabase(db, state => { state.problems[id] = { ...state.problems[id], ...problem, } return state.problems[id] }) export const deleteProblem = (db: DatabaseConnection, id: ProblemId): Promise => withDatabase(db, state => { state.problems[id].deleted = true }) // // Solutions // export const createSolution = ( db: DatabaseConnection, { sentBy, forProblem, content }: Omit ): Promise => withDatabase(db, state => { const id = crypto.randomBytes(10).toString('hex') as SolutionId state.solutions[id] = { id, createdAt: new Date().toISOString(), deleted: false, sentBy, forProblem, content, status: 'pending', visible: false, } return id }) export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise => withDatabase(db, state => { const solution = (state.solutions[id] ?? null) as Solution | null return solution && !solution.deleted ? solution : null }) export const getSolutions = (db: DatabaseConnection) => withDatabase(db, state => { return Object.values(state.solutions).filter(s => !s.deleted) }) export const getVisibleSolutions = (db: DatabaseConnection): Promise => withDatabase(db, state => { return Object.values(state.solutions) .filter(s => !s.deleted) .filter(s => s.visible) }) export const updateSolution = (db: DatabaseConnection, id: SolutionId, solution: Partial>): Promise => withDatabase(db, state => { state.solutions[id] = { ...state.solutions[id], ...solution, } return state.solutions[id] }) export const deleteSolution = (db: DatabaseConnection, id: SolutionId): Promise => withDatabase(db, state => { state.solutions[id].deleted = true })