Created
April 28, 2023 07:09
-
-
Save seia-soto/7d9551c0a18593b492d55f01270efa6f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {type Static, type TSchema} from '@sinclair/typebox'; | |
import {TypeCompiler} from '@sinclair/typebox/compiler'; | |
import {rename, writeFile} from 'fs/promises'; | |
import path from 'path'; | |
import {isFile, readFileSafe} from '../fs/safe.js'; | |
import {debounce} from '../utils/debounce.js'; | |
export class ConfigProvider<T extends TSchema> { | |
absFilename: string; | |
local: Static<T>; | |
validator: ReturnType<typeof TypeCompiler.Compile<T>>; | |
write = debounce(async () => this.writeImmediately(), 1000); | |
constructor( | |
readonly filename: string, | |
readonly schema: T, | |
readonly defaults: Static<T>, | |
) { | |
this.absFilename = path.join(process.cwd(), filename); | |
this.local = defaults; | |
// eslint-disable-next-line new-cap | |
this.validator = TypeCompiler.Compile(schema); | |
} | |
validate(data: unknown): data is Static<T> { | |
// eslint-disable-next-line new-cap | |
return this.validator.Check(data); | |
} | |
async bootstrap() { | |
if (!await isFile(this.absFilename)) { | |
await this.writeImmediately(); | |
return; | |
} | |
if (!await this.sync()) { | |
await rename(this.absFilename, this.absFilename + '.old'); | |
await this.writeImmediately(); | |
} | |
} | |
async sync() { | |
if (!await isFile(this.absFilename)) { | |
return false; | |
} | |
const file = await readFileSafe(this.absFilename, 'utf-8'); | |
try { | |
const data = JSON.parse(file) as unknown; | |
if (this.validate(data)) { | |
this.local = data; | |
return true; | |
} | |
return false; | |
} catch (_error) { | |
return false; | |
} | |
} | |
private async writeImmediately() { | |
await writeFile(JSON.stringify(this.local), 'utf-8'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const debounce = <F extends ((...args: any[]) => any)>(fn: F, timeout: number) => { | |
let timer: ReturnType<typeof setTimeout>; | |
return (...args: Parameters<F>) => { | |
clearTimeout(timer); | |
timer = setTimeout(() => { | |
fn.apply(this, args); | |
}, timeout); | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {type PathLike, existsSync} from 'fs'; | |
import {readFile, stat} from 'fs/promises'; | |
/** | |
* Reads the file safely on non-UNIX environment | |
*/ | |
export const readFileSafe = async (...args: Parameters<typeof readFile>) => { | |
const handle = await readFile(...args); | |
return handle.toString().replace(/\r/g, ''); | |
}; | |
/** | |
* Checks if file exists at the path | |
*/ | |
export const isFile = async (path: PathLike) => { | |
if (!existsSync(path)) { | |
return false; | |
} | |
const ret = await stat(path); | |
if (!ret.isFile()) { | |
return false; | |
} | |
return true; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment