413 lines
8.7 KiB
JavaScript
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,
|
|
};
|
|
|