UNPKG

microwizard

Version:

A fast and stable microservice framework, mostly compatible with senecas user API

582 lines (483 loc) 12.7 kB
/* const initialSenecaConfig = { auto: true, listen: [{ pin: 'service:user,command:*', model: 'consume', type: 'tcp' }], discover: { rediscover: true, custom: { active: true, find: dnsSeed } } }; */ // request.seneca.actAsync( // 'service:inventory,command:create,asset:quota,type:w1', // { session, data ... } // ); import tp from './transfer.js'; import Balancer from './balance.js'; import { uid } from 'uid'; import Promise from 'bluebird'; import Mesh from './mesh.js'; const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time)); const start = process.hrtime.bigint(); export const transport = tp; const convertPin = (pin) => { const r = /([a-zA-Z0-9_-]+)(?:\s+)?:([^,]+)/g; let t; let final = []; if (typeof pin === 'object') { final = Object.entries(pin).map(([x, y]) => { if (y !== '*') { return { p: `${x}:${y}`, c: 1 }; } else { return { p: x, c: 2 }; } }); } else { while ((t = r.exec(pin))) { if (t[2] !== '*') { final.push({ p: t[0], c: 1 }); } else { final.push({ p: t[1], c: 2 }); } } } return { c: final.sort((a, b) => (a.p < b.p ? -1 : a.p < b.p ? 1 : 0)) }; }; export const convertData = (pin) => { const r = /([a-zA-Z0-9_-]+)(?:\s+)?:([^,]+)/g; let t; const final = []; const o = {}; while ((t = r.exec(pin))) { final.push(t[0]); o[t[1]] = t[2]; } return { c: final.sort(), o }; }; export const pattern = (pin) => { const { c } = convertData(pin); return c.join(','); }; export const utilClean = function (obj, opts) { if (obj == null) return obj; const out = Array.isArray(obj) ? [] : {}; const pn = Object.getOwnPropertyNames(obj); for (const pnI of pn) { const p = pnI; if (p[p.length - 1] !== '$') { out[p] = obj[p]; } } return out; }; const FORBIDDEN = { undefined: true, object: true, function: true, symbol: true }; export default class Micro { #pinDB = { n: {}, s: {} }; #hash = {}; #pinCache = {}; #convCache = {}; #register = {}; #balancer; #transport; #stopped = false; #active = 0; #clients = {}; #loaded = {}; constructor (options = {}) { if (options.debug) { setInterval(() => { console.dir(this.#clients, { depth: null }); }, 4000); } this.root = this; this.log = { error: (m, e) => console.error(m, e), info: (m, e) => console.log(m, e) }; this.util = { Jsonic: (x) => convertData(x).o, clean: utilClean }; this.actAsync = this.act; this.id = uid(); this.#balancer = new Balancer(options.balance); this.#transport = transport(options.transport, this); this.register('role:mc,cmd:close', async function (msg) { this.#stopped = true; while (this.#active > 0) { await sleep(500); } // give the last msgs a chance to be send out await sleep(500); await this.#transport.tp.close(); return { code: 200, msg: 'shutdown' }; }); this.register('role:transport,cmd:listen', function (msg) { return Promise.fromCallback((reply) => this.#transport.listen(msg.config, reply, () => this.reduceActive()) ); }); } get offline () { return this.#stopped; } // no emitter yet on () {} delegate () { // in esm we can't do that, not so important for us though return this; // const del = Object.create(this); // del.did = del.did ? `${del.did}/` : '' + uid(); // return del; } async client (config) { const { type, pin } = config; if (type === 'balance') { this.#clients[pin] = this.#balancer.addClient(config); this.#clients[pin].handle = this.#balancer.makeHandle(config); } else if (type === 'web') throw new Error('not supported'); else { const client = await Promise.fromCallback((reply) => this.#transport.client(config, reply) ); const me = this; let cl; if (me.#clients[pin]) { cl = me.#clients[pin]; const action = function (msg, meta) { return client.send.call(this, msg, meta).catch((x) => { throw x; }); }; action.id = config.id; cl.handle(pin, action); } const func = function (msg, meta) { if (meta.pin) { meta.k = 'aE'; meta.p = meta.pin; } if (cl) { return cl.send.call(this, msg, meta).catch((x) => { throw x; }); } else { return client.send.call(this, msg, meta).catch((x) => { throw x; }); } }; func.client = true; if (Array.isArray(pin)) { pin.map((pin) => this.add(pin, func)); } else { this.add(pin, func); } } } removeClient (msg) { return this.#balancer.removeClient(msg); } use (module, options) { switch (module) { case 'mesh': case 'mesh-ng': this.#loaded.mesh = 'loading'; Mesh(options, this); setTimeout(async () => { await this.callInternal('init:mesh', {}); this.#loaded.mesh = true; }, 500); break; } } ready (cb) { if (this.#loaded.mesh === 'loading') { setTimeout(() => { this.ready(cb); }, 500); return false; } cb(); return true; } listen (msg) { return this.callInternal('role:transport,cmd:listen', { config: msg }); } callInternal (pin, args) { return this.#register[pin][0].call(this, args, { id: uid(), pin }); } register (pin, method) { this.#register[pin] = this.#register[pin] || []; this.#register[pin].unshift(method); } async rPrior (msg, meta) { if (!meta.priorI) { meta.priorI = 0; } const pin = this.#register[meta.pin]; const method = pin[++meta.priorI]; if (method) return method.call(this, msg); else return {}; } async prior (msg, meta) { if (!meta.priorI) { meta.priorI = 0; } const pin = this.#matchPin(meta.pin); const method = pin.fl[meta.priorI++]; if (method) return method.call(this, msg); else return {}; } async actE (x, y, opts = {}, meta = {}) { meta.id = meta.id || uid(); if (opts.mixin?.length) { for (const m of opts.mixin) { x = `${x},${m}:${y[m]}`; } } if (y && this.#pinCache[x]) { const { pin, xConv } = this.#pinCache[x]; meta.pin = meta.pin || x; return pin.f({ data: y, msg: xConv }, meta); } let pat; let patF; let xConv; if (typeof x === 'object') { pat = Object.entries(x).reduce((o, [x, y]) => { if (!FORBIDDEN[typeof y]) { o.push(`${x}:${y}`); } return o; }, []); xConv = x; } else { const conv = convertData(x); pat = conv.c; xConv = conv.o; } // if (typeof (y) === 'object') { // patF = Object.entries(y).reduce((o, [x, y]) => { // if (!FORBIDDEN[typeof (y)]) { // o.push(`${x}:${y}`); // } // return o; // }, []); // } else if (typeof (y) === 'string') { // const conv = convertData(y); // patF = conv.c; // y = conv.o; // } else { // y = xConv; // } pat = pat.sort(); const pin = this.#matchPin([], pat); if (typeof x === 'string' && pin.f) { this.#pinCache[x] = { xConv, pin }; } if (!pin.f) { return console.log('no-target', pat); } meta.pin = meta.pin || x; return pin.f({ data: y, msg: xConv }, meta); } find (x) { let pat; let xConv; if (typeof x === 'string' && this.#convCache[x]) { ({ xConv, pat } = this.#convCache[x]); } else if (typeof x === 'object') { pat = Object.entries(x) .reduce((o, [x, y]) => { if (!FORBIDDEN[typeof y]) { o.push(`${x}:${y}`); } return o; }, []) .sort(); xConv = x; } else { const conv = convertData(x); pat = conv.c; xConv = conv.o; this.#convCache[x] = { pat, xConv }; } pat = pat.sort(); const pin = this.#matchPin(pat); return pin.f; } async act (x, y, meta = {}) { let pat; let xConv; if (typeof x === 'string' && this.#convCache[x]) { ({ xConv, pat } = this.#convCache[x]); } else if (typeof x === 'object') { pat = Object.entries(x) .reduce((o, [x, y]) => { if (!FORBIDDEN[typeof y]) { o.push(`${x}:${y}`); } return o; }, []) .sort(); xConv = x; } else { const conv = convertData(x); pat = conv.c; xConv = conv.o; this.#convCache[x] = { pat, xConv }; } if (typeof y === 'object') { pat = [ ...pat, ...Object.entries(y).reduce((o, [x, y]) => { if (!FORBIDDEN[typeof y]) { o.push(`${x}:${y}`); } return o; }, []) ]; } else if (typeof y === 'string') { const conv = convertData(x); pat = [...pat, conv.c]; y = conv.o; } else { y = xConv; } pat = pat.sort(); const pin = this.#matchPin(pat); if (!pin.f) { return console.log('no-target', pat); } meta.id = meta.id || uid(); return pin.f({ pin$: pin, data: Object.assign({}, xConv, y) }, meta); } // this simply iterates over every object part since they are sorted non // matching elements are no big problem they are skipped over. // A one to one check is tested first, next a functional map is tested // we only support the wildcard (*) function as of now. #matchPin (c, h) { let before = this.#pinDB; let t = c; const beforeParts = []; if (h) { before = this.#hash[h.join(',')] || before; t = h; } let u = 0; const ll = { f: 0 }; for (const o of t) { let part; const bP = beforeParts.length; for (let i = 0; i < bP; ++i) { const beforePart = beforeParts[i].part; const b = beforePart; if (b.n[o]) { beforeParts[i].d++; beforeParts[i].part = b.n[o]; if ((part = b.s[o.split(':')[0]])) { beforeParts.push({ d: beforeParts[i].d, part }); if (i + 1 === beforeParts.length) { beforeParts.push({ d: 0, part: this.#pinDB }); } } } else if ((part = b.s[o.split(':')[0]])) { // this equals a * map beforeParts[i].d++; beforeParts[i].part = part; } if (beforeParts[i].d > ll.f) { ll.f = beforeParts[i].d; ll.p = beforeParts[i].part; } } if (bP === 0) { beforeParts.push({ d: 0, part: this.#pinDB }); } part = undefined; if (before.n[o]) { ++u; before = before.n[o]; if ((part = before.s[o.split(':')[0]])) { beforeParts.push({ d: u, part }); } } else if ((part = before.s[o.split(':')[0]])) { // this equals a * map ++u; before = part; } } if ((!before.f || ll.f > u) && ll.p?.f) { before = ll.p; } if (!before.f) { return { o: 'err' }; } else { return before; } } reduceActive () { this.#active--; } add (c, fu) { let f; if (process.env.METRICS) { f = (d, meta) => { if (meta.n === true) { ++this.#active; } meta.start = Number(process.hrtime.bigint() - start); return fu.call(this, d.data, meta); }; } else { f = (d, meta) => { if (meta.n === true) { ++this.#active; } return fu.call(this, d.data, meta); }; } Object.entries(fu).forEach(([x, y]) => { f[x] = y; }); let before = this.#pinDB; const pin = convertPin(c); let i = 0; for (; i < pin.c.length; ++i) { const o = pin.c[i].p; if (pin.c[i].c === 1) { if (before.n[o]) { before = before.n[o]; } else { before.n[o] = { n: {}, s: {} }; before = before.n[o]; } } else { if (before.s[o]) { // this equals a * map before = before.s[o]; } else { before.s[o] = { s: {}, n: {} }; before = before.s[o]; } } } this.#hash[pin.c.join(',')] = before; before.f = f; before.fl = before.fl || []; before.fl.unshift(f); return before; } async close () { return this.callInternal('role:mc,cmd:close', {}); } }