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((/\//, /^(?:(?[^[:]*)|\[(?[^[]*)\])(?::(?\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, };