diff --git a/src/server/lib/dbfile.ts b/src/server/lib/dbfile.ts new file mode 100644 index 0000000..fc4afad --- /dev/null +++ b/src/server/lib/dbfile.ts @@ -0,0 +1,154 @@ +import { readFile, writeFile, rename, readdir } from "fs/promises" +import path from "node:path" + +const DATADIR = `./.data` +const TICK = 500 + +export type Write = ReturnType + +// this is fucking stupid why did i write this + +class Activity { + + _write: () => Promise + destroy: () => void + + goal: number = Date.now() + lastWrt: number = Date.now() + clock? : { type: "precise", id: NodeJS.Timeout } | { type: "tick", id: NodeJS.Timeout, lastGoal: number } + + constructor(writeFunc: () => Promise, destroyFunc: () => void) { + this._write = writeFunc + this.destroy = destroyFunc + } + + write() { + this.lastWrt = Date.now(); + return this._write() + } + + finish() { + this.stopClock() + this.write() + this.destroy() + } + + tick() { + if (!this.clock || !("lastGoal" in this.clock)) return + if (Date.now() > this.goal) return this.finish(); + + if (this.goal == this.clock.lastGoal) + this.startPreciseClock() + else if (Date.now()-this.lastWrt < 15000) + this.write() + } + + stopClock() { + if (!this.clock) return + else clearTimeout(this.clock.id) + } + + startTickClock() { + this.stopClock() + this.clock = { + type: "tick", + id: setInterval(this.tick.bind(this), TICK), + lastGoal: this.goal + } + } + + startPreciseClock() { + this.stopClock() + this.clock = { + type: "precise", + id: setTimeout(this.finish.bind(this), this.goal-Date.now()) + } + } + + set() { + this.goal = Date.now()+5000 + if (!this.clock || this.clock.type != "precise") + this.startPreciseClock() + } + +} + +export default class DbFile { + + name: string + data: Structure + activity?: Activity + + private writeInProgress?: Promise + private rewriteNeeded: boolean = false + private readonly files: string[] + + constructor(name: string, defaultData: Structure) { + this.name = name + this.data = defaultData + this.files = [`${name}.json`, `${name}-b.json`].map(e => path.join(DATADIR, e)) + } + + private async findAvailable() { + return (await readdir(DATADIR)) + .filter(e => e.match(new RegExp(`^${this.name}(?:-b)?.json$`))) + .map(e => path.join(DATADIR, e)) + } + + /** + * @description Write files to disk; doesn't care about preventing corruption aside from the 2 copies + */ + private async write() { + + let data = JSON.stringify(this.data) + for (let x in this.files) + await writeFile(x, data) + + } + + /** + * @description Write files to disk; checks if a write is in progress first + */ + private async queueWrite(): Promise { + if (this.writeInProgress) { // if write in progress + this.rewriteNeeded = true // signify that a rewrite is needed + return this.writeInProgress + } + + this.writeInProgress = this.write() + await this.writeInProgress; // wait for it to complete + delete this.writeInProgress; // then remove it + + if (this.rewriteNeeded) return this.queueWrite() // queues up another write if needed + } + + /** + * @description Starts saving data to disk + */ + async save() { + if (!this.activity) + this.activity = + new Activity( + this.queueWrite.bind(this), + () => delete this.activity + ) + + this.activity.set() + } + + private async tryRead(path: string) { + return JSON.parse(readFile(path).toString()) + } + + async read() { + let availFiles = await this.findAvailable() + for (let x in availFiles) { + let data = await this.tryRead(x).catch(e => null) + if (data !== null) { + this.data = data + break + } + } + } + +} \ No newline at end of file