Skip to content

Instantly share code, notes, and snippets.

@J-Cake
Last active January 7, 2022 04:23
Show Gist options
  • Save J-Cake/b17b15305b61d9359a1f9f77712b525f to your computer and use it in GitHub Desktop.
Save J-Cake/b17b15305b61d9359a1f9f77712b525f to your computer and use it in GitHub Desktop.
Very simple table system which can use files on disk
import fs from 'fs';
import cp from 'child_process';
import readline from 'readline';
export enum Status {
Success = 0,
Unauthorised,
Nonexistent,
Invalid
}
export enum Type {
Text,
DateTime,
TimeSpan,
Int,
Float,
Path,
}
type Primitive<T> = {
serialise(obj: T): string,
parse(str: string): T
};
type Value<T extends Type> = typeof parsers[T] extends Primitive<infer K> ? K : never;
type RowValue<Row extends TableRow> = { [K in keyof Row]: Value<Row[K]> };
export class TimeSpan {
constructor(public readonly days: number,
public readonly hours: number,
public readonly minutes: number,
public readonly seconds: number,
public readonly ms: number) {
}
}
const parsers: Record<Type, Primitive<any & { readonly _type: Type }>> = {
[Type.Text]: {
serialise: (obj: string) => `"${encodeURIComponent(obj)}"`,
parse: (str: string) => decodeURIComponent(str.slice(1, -1))
},
[Type.DateTime]: {
serialise: (obj: Date) => `{${obj.toISOString()}}`,
parse: (str: string) => new Date(str.slice(1, -1))
},
[Type.TimeSpan]: {
serialise: (obj: TimeSpan) => `[${obj.days}-${obj.hours}-${obj.minutes}-${obj.seconds}-${obj.ms}]`,
parse: (str: string) => new TimeSpan(...str.slice(1, -1).split('-').map(i => Number(i)) as [number, number, number, number, number])
},
[Type.Int]: {
serialise: (obj: bigint) => `${obj.toString()}n`,
parse: (str: string) => BigInt(str.slice(0, -1))
},
[Type.Float]: {
serialise: (obj: number) => obj.toString(),
parse: (str: string) => Number(str)
},
[Type.Path]: {
serialise: (obj: fs.promises.FileHandle) => `//${cp.execSync(`lsof -a -p ${process.pid} -d ${obj} -Fn | tail -n +3`).slice(1)}`,
parse: (str: string) => fs.promises.open(str.slice(2), 'r')
},
}
export type TableRow = { [column: string]: Type };
export type TableHeader<T> = { [column in keyof T]: [index: number, type: Type] };
export type Matcher<Row extends TableRow> = (row: Row) => boolean;
export default class DBTable<Row extends TableRow> {
public static readonly DELIMITER = ',';
private file: fs.promises.FileHandle;
private table: RowValue<Row>[] = [];
private readonly: boolean = false;
private constructor(private header: TableHeader<Row>) {
}
static open<Row extends TableRow>(file: fs.promises.FileHandle, readonly: boolean = false): Promise<DBTable<Row>> {
return new Promise(function (resolve, reject) {
let db: DBTable<Row>;
const lines = readline.createInterface({input: file.createReadStream({start: 0, autoClose: false})})
lines.once('line', line => {
db = new DBTable<Row>(this.parseHeader(line));
lines.on('line', line => db.table.push(db.parse(line)));
}).on('close', function() {
if (!db)
reject(Status.Nonexistent);
db.file = file;
db.readonly = readonly;
resolve(db);
}.bind(this));
}.bind(this));
}
private static parseHeader<T extends TableRow>(row: string): TableHeader<T> {
const header: Partial<TableHeader<T>> = {};
for (const [a, column] of row.split(DBTable.DELIMITER).entries()) {
const [name, type] = column.split(':');
header[name.trim() as keyof TableHeader<T>] = [a, Type[type.trim()]];
}
return header as TableHeader<T>;
}
public async close() {
const keysWithFiles = Object.keys(this.header).filter(i => this.header[i][1] === Type.Path)
const isFileHandle: (x: any) => x is fs.promises.FileHandle = (x): x is fs.promises.FileHandle => typeof x === 'object' && 'fd' in x && 'close' in x;
console.log(new Date().toISOString(), '\tClosing Database');
for (const i of this.table)
for (const key of keysWithFiles)
if (isFileHandle(i[key]))
await (i[key] as fs.promises.FileHandle).close();
await this.file.close();
}
public async push(value: RowValue<Row>): Promise<Status.Success> {
if (this.readonly)
throw Status.Unauthorised;
this.table.push(value);
return Status.Success
}
public async* select(match: Matcher<RowValue<TableRow>>): AsyncGenerator<RowValue<TableRow>> {
let _line;
for await (const line of readline.createInterface({
input: this.file.createReadStream({
start: 0,
autoClose: false
})
}))
if (!_line)
_line = line;
else {
const row = this.parse(line);
if (match(row))
yield row
}
}
public commit(): this {
const stream = this.file.createWriteStream({start: 0, autoClose: false});
stream.write(Object.keys(this.header).map(i => `${i}:${Type[this.header[i][1]]}`).join(DBTable.DELIMITER) + "\n");
for (const i of this.table)
stream.write(this.serialise(i) + "\r\n");
return this;
}
private parseValue(i: string): Row[string] {
if (i.startsWith('//'))
return parsers[Type.Path].parse(i);
else if (i.endsWith('n') && !isNaN(Number(i.slice(0, -1))))
return parsers[Type.Int].parse(i);
else if (!i.endsWith('n') && !isNaN(Number(i)))
return parsers[Type.Float].parse(i);
else if (i.startsWith('{') && i.endsWith('}'))
return parsers[Type.DateTime].parse(i);
else if (i.startsWith('[') && i.endsWith(']'))
return parsers[Type.TimeSpan].parse(i);
else if (i.startsWith('"') && i.endsWith('"'))
return parsers[Type.Text].parse(i);
else throw `Unrecognised value '${i}'`;
}
private parse(line: string): RowValue<Row> {
const pieces = line.split(DBTable.DELIMITER);
const obj: Partial<Row> = {};
for (const [a, i] of pieces.entries())
obj[Object.keys(this.header)[a] as keyof Row] = this.parseValue(i.trim());
return obj as RowValue<Row>;
}
private serialise(value: RowValue<Row>): string {
const strings: { [key: string]: string } = {};
for (const i in value)
if (value[i] as any instanceof TimeSpan)
strings[i] = parsers[Type.TimeSpan].serialise(value[i]);
else if (value[i] as any instanceof Date)
strings[i] = parsers[Type.DateTime].serialise(value[i]);
else if (typeof value[i] === 'number')
strings[i] = parsers[Type.Float].serialise(value[i]);
else if (typeof value[i] === 'string')
strings[i] = parsers[Type.Text].serialise(value[i]);
else if (typeof value[i] === 'bigint')
strings[i] = parsers[Type.Int].serialise(value[i]);
const str: Set<string> = new Set(); // ensure the order is preserved
for (const key of Object.keys(this.header))
str.add(strings[key]);
return Array.from(str).join(DBTable.DELIMITER) + '\n';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment