klh-util.js/index.js
2026-07-06 00:24:01 +05:00

413 lines
8.7 KiB
JavaScript

var dgram = require('node:dgram');
var net = require('node:net');
var randstr = (fun => len => {
let out = [];
if (out.length < len)
for (let c of fun()) {
if (out.length == 0 && c.match(/[0-9]/))
continue;
out.push(c);
if (out.length >= len)
break;
}
return out.join('');
})(function*() {
while (1) {
let ch = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(3))));
if (!ch.match(/[\/+=]/)) {
for (let c of ch) {
yield c;
}
}
}
});
function rinfostr(rinfo) {
return `${(rinfo.address+'').match(/:/)?`[${rinfo.address}]`:rinfo.address}:${rinfo.port}`;
}
function unrinfostr(str) {
if (str == null) throw new TypeError('Address string is missing')
return [(str+'').match((/\//, /^(?:(?<ip>[^[:]*)|\[(?<ip6>[^[]*)\])(?::(?<port>\d*))?$/))
].map(o => { if (!o) throw new SyntaxError('Invalid address string'); return o.groups })
.map(({ ip, ip6, port }) => ({ port: port ? parseInt(port) : undefined, address: ip ?? ip6 }))[0]
}
async function sockWaitFor(socket, ev) {
var ok, err;
await new Promise((a, b) => {
[ok, err] = [a, b];
socket.on('error', err);
socket.on(ev, ok);
});
socket.off('error', err);
socket.off(ev, ok);
}
async function mapAI(ai, fn) {
for await (let obj of ai) {
await fn(obj);
}
}
function composeAI(gen, ai, ...arg) {
return gen(ai[Symbol.asyncIterator](), ...arg);
}
async function bindUdp(...arg) {
var server;
await (async fun => {
// this is stupid but im stupid too so its ok
try {
server = dgram.createSocket('udp6');
await fun();
} catch {
try {
server.close();
} catch {}
server = dgram.createSocket('udp4');
await fun();
}
})(async () => {
server.bind(...arg);
await sockWaitFor(server, 'listening');
});
return server;
}
async function connectUdp(...arg) {
var client;
await (async fun => {
// see ${_FILE}:${_LINE - 18}
try {
client = dgram.createSocket('udp6');
await fun();
} catch {
try {
client.close();
} catch {}
client = dgram.createSocket('udp4');
await fun();
}
})(async () => {
client.bind(0);
await sockWaitFor(client, 'listening');
client.connect(...arg);
await sockWaitFor(client, 'connect');
});
return client
}
async function bindTcp(...arg) {
var server = net.createServer();
server.listen(...arg);
await sockWaitFor(server, 'listening');
return server;
}
async function connectTcp(...arg) {
var client = new net.Socket();
client.connect(...arg);
await sockWaitFor(client, 'connect');
return client;
}
// timeout but u can bump it so it fires later
function Bumpout(fun, time) {
this._callback = (...args) => {
this._handle = null;
return fun(...args);
}
this.timeout = time;
this.when = Infinity;
this.restart();
}
Object.assign(Bumpout.prototype, {
bump() {
if (!this._handle) return false;
return this.restart();
},
cancel() {
if (!this._handle) return false;
clearTimeout(this._handle);
this._handle = null;
return true;
},
restart(timeout) {
if (this._handle) this.cancel();
this.when = Date.now() + this.timeout;
this._handle = setTimeout(this._callback, timeout ?? this.timeout);
return true;
},
});
function DataGenerator() {
this._gcs = [];
this._cond = null;
}
Object.assign(DataGenerator.prototype, {
async *[Symbol.asyncIterator]() {
while (1) {
let gencond = this._gcs.shift();
if (gencond)
gencond();
let val = await new Promise((ok) => {
this._cond = ok;
});
if (val == null) break;
yield val;
}
},
async push(msg) {
if (!this._cond) {
let gencond;
let genprom = new Promise((ok) => {
gencond = ok;
});
this._gcs.push(gencond);
await genprom;
}
this._cond(msg);
this._cond = null;
},
});
function AIDechunkinator(ai) {
ai = ai[Symbol.asyncIterator]();
this._ai = {
[Symbol.asyncIterator]() { return this; },
next() { return ai.next(); }
};
this._chunk = null;
this._idx = null;
}
Object.assign(AIDechunkinator.prototype, {
async *bytes() {
while (this._chunk) {
let idx = this._idx; this._idx++;
let v = this._chunk[idx];
if (this._idx >= this._chunk.length)
this._chunk = null;
yield v;
}
for await (let v of this._ai) {
this._chunk = v;
this._idx = 0;
while (this._chunk) {
let idx = this._idx; this._idx++;
let v = this._chunk[idx];
if (this._idx >= this._chunk.length)
this._chunk = null;
yield v;
}
}
},
async *chunks() {
if (this._chunk) {
let chunk = this._chunk;
this._chunk = null;
yield chunk.slice(this._idx);
}
for await (let v of this._ai) {
yield v;
}
},
async read(len, lax) {
let chunk = [];
if(len <= 0) return chunk;
for await (let v of this.bytes()) {
if (v == null)
return;
chunk.push(v);
if (chunk.length >= len)
break;
}
if((!lax) && chunk.len < len)
throw new Error("Truncated Read");
return chunk;
},
});
function duocolorize(asyfun) {
return function(...arg) {
let okval, errval;
let prom = asyfun.call(this,
val => (okval = [val])[0],
err => {
throw (errval = [err])[0];
},
...arg
);
if (errval != null) {
prom.catch(() => {});
throw errval[0];
}
if (okval != null) {
prom.catch(() => {});
return okval[0];
}
return prom;
}
}
function isThenable(val) {
return typeof val.then == "string";
}
function DbMigrator(db, version) {
this.stmts = {};
this.db = db;
this.dbVersion = version;
this.migrations = Object.create({
add(from, to, data){
this[from] = this[from] ?? {};
this[from][to] = data;
}
});
}
// Assumes db interface similar to better-sqlite3 but possibly async
Object.assign(DbMigrator.prototype, {
getStmt: duocolorize(async function (res, rej, def) {try{
let stmt;
return res(stmts[def] ?? (
this.stmts[def] = isThenable(stmt = db.prepare(def))
? (await stmt)
: stmt
));
}catch(e){rej(e)}}),
setDb(db) {
this.db = db;
this.stmts = {};
},
getDbVersion() {
return this.db.transaction(duocolorize(async () => {try{
try {
let version = this.getStmt("SELECT * FROM db_version;");
version = isThenable(version) ? (await version) : version;
version = version.get();
version = isThenable(version) ? (await version) : version;
version = version.version;
if (!version) {
throw null;
}
return res(version-0);
} catch(e) {
if (!db.inTransaction) throw e;
return res(-1);
}
}catch(e){rej(e)}}))();
},
setDbVersion(version) {
return this.db.transaction(duocolorize(async (res, rej) => {try{
let prom;
prom = this.getStmt("DELETE FROM db_version;")
.run();
if (isThenable(prom)) await prom;
prom = this.getStmt("INSERT INTO db_version (version) VALUES (@version);")
.run({version});
if (isThenable(prom)) await prom;
}catch(e){rej(e)}})).immediate();
},
migrate: duocolorize(async function(res, rej) {try{
var prom;
let dbVersion = this.dbVersion;
let currentDbVersion = isThenable(prom = this.getDbVersion())
? (await prom)
: prom;
if(dbVersion != currentDbVersion) {
let path = [];
let seen = {};
let que = [[currentDbVersion,[]]];
while (que.length) {
let que2 = [];
for (let o of que) {
if (o[0] == dbVersion) {
que2 = [];
path = o[1];
break;
}
seen[o[0]] = 1;
for (let e of Object.keys(this.migrations[o[0]]??{}).map(e=>e-0)) {
if (!seen[e]) {
que2.push([e,o[1].concat([e])])
}
}
}
que = que2;
}
console.log("db migration path:", [currentDbVersion].concat(path));
isThenable(prom = this.db.transaction(duocolorize(async (res, rej) => {try{
var prom;
for (let ver of path) {
console.log("db migration from",currentDbVersion,"to",ver)
if((isThenable(prom = this.getDbVersion()) ? (await prom) : prom) != currentDbVersion) {
throw new Error("corrupted db version");
}
for (let migr of this.migrations[currentDbVersion][ver]) {
console.log('db migration: ',e);
if (typeof e == "string") {
isThenable(prom = (isthenable(prom = this.db.prepare(e))
? (await prom)
: prom).run())
? (await prom)
: prom;
} else {
isThenable(prom = e())
? (await prom)
: prom;
}
}
currentDbVersion = ver;
}
if((currentDbVersion = isThenable(prom = this.getDbVersion())
? (await prom)
: prom) != dbVersion) {
throw new Error("malformed database version: want "+dbVersion+", got "+currentDbVersion);
}
}catch(e){rej(e)}})).immediate()) && (await prom);
}
return res();
}catch(e){rej(e)}}),
});
module.exports = {
randstr,
rinfostr,
unrinfostr,
sockWaitFor,
mapAI,
composeAI,
bindUdp,
connectUdp,
bindTcp,
connectTcp,
Bumpout,
DataGenerator,
AIDechunkinator,
duocolorize,
isThenable,
DbMigrator,
};