import crypto from 'crypto' import { readFile, writeFile, access, constants } from 'fs/promises' import { MetadataProps 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 } 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(`[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) => Promise = (db, id) => withDatabase(db, state => { 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] ?? null }) 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') as SolutionId state.solutions[id] = { id, createdAt: new Date().toISOString(), sentBy, forProblem, content, status: 'pending', visible: false, } return id }) export const getSolution = (db: DatabaseConnection, id: SolutionId): Promise => withDatabase(db, state => { return state.solutions[id] ?? null }) 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 getSolutions = (db: DatabaseConnection) => withDatabase(db, state => { let solutions = Object.values(state.solutions) return solutions }) export const getVisibleSolutions = (db: DatabaseConnection) => withDatabase(db, state => { return Object.values(state.solutions).filter(s => s.visible) })