Skip to content

Instantly share code, notes, and snippets.

@seia-soto
Created April 28, 2023 07:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seia-soto/7d9551c0a18593b492d55f01270efa6f to your computer and use it in GitHub Desktop.
Save seia-soto/7d9551c0a18593b492d55f01270efa6f to your computer and use it in GitHub Desktop.
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');
}
}
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);
};
};
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