import crypto from 'crypto' import { readFile, writeFile, access, constants } from 'fs/promises' import { CommonProps as MetaProps, Problem, ProblemId, Solution, SolutionId, User, UserId, } 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 } interface Lock { (): void } interface Mutex { lock(): Promise } function createMutex(): Mutex { let locked = false const waiters: ((fn: () => void) => void)[] = [] const unlock = () => { if (waiters.length > 0) { console.log(`[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(`[Mutex] Releasing the lock`) } } const lock = (): Promise => { if (locked) { console.log(`[Mutex] Putting into queue`) return new Promise(resolve => { waiters.push(resolve) }) } else { console.log(`[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) => withDatabase(db, (state: Database): User | null => { return state.users[id] ?? null }) // // Problems // export const createProblem = ( db: DatabaseConnection, { content, createdBy }: Omit ): Promise => withDatabase(db, state => { const nextId = (Object.keys(state.problems).length + 1).toString() as ProblemId state.problems[nextId] = { id: nextId, content, createdBy, createdAt: new Date().toJSON(), } return nextId }) export const getProblem = (db: DatabaseConnection, id: string): Promise => withDatabase(db, state => { return state.problems[id] }) export const getProblems = (db: DatabaseConnection): Promise => withDatabase(db, state => { return Object.values(state.problems) }) // // Solutions // export const createSolution = ( db: DatabaseConnection, { sentBy, forProblem, content }: Omit ): Promise => withDatabase(db, state => { const id = crypto.randomBytes(10).toString('hex') state.solutions[id] = { id, sentBy, forProblem, content, status: 'pending', } return id }) export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise => withDatabase(db, state => { return state.solutions[id] }) export const updateSolution = ( db: DatabaseConnection, id: SolutionId, solution: Omit ): Promise => withDatabase(db, state => { state.solutions[id] = { id, ...solution } return state.solutions[id] }) type SolutionsQuery = Partial<{ sentBy: UserId forProblem: ProblemId }> export const getSolutions = (db: DatabaseConnection, { sentBy, forProblem }: SolutionsQuery = {}) => withDatabase(db, state => { let solutions = Object.values(state.solutions) if (sentBy) { solutions = solutions.filter(s => s.sentBy === sentBy) } if (forProblem) { solutions = solutions.filter(s => s.forProblem === forProblem) } return solutions })