UNPKG

seneca

Version:

A Microservices Framework for Node.js

765 lines 25.7 kB
"use strict"; /* Copyright © 2010-2023 Richard Rodger and other contributors, MIT License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.API = void 0; const gubu_1 = require("gubu"); const common_1 = require("./common"); const Argu = (0, gubu_1.MakeArgu)('seneca'); const errlog = common_1.make_standard_err_log_entry; const intern = {}; function wrap(pin, actdef, wrapper) { const pinthis = this; wrapper = 'function' === typeof actdef ? actdef : wrapper; actdef = 'function' === typeof actdef ? {} : actdef; pin = Array.isArray(pin) ? pin : [pin]; (0, common_1.each)(pin, function (p) { (0, common_1.each)(pinthis.list(p), function (actpattern) { pinthis.add(actpattern, wrapper, actdef); }); }); return this; } function fix(patargs, msgargs, custom) { const self = this; patargs = self.util.Jsonic(patargs || {}); const fix_delegate = self.delegate(patargs); // TODO: attach msgargs and custom to delegate.private$ in some way for debugging // TODO: this is very brittle. Use a directive instead. fix_delegate.add = function fix_add() { return self.add.apply(this, intern.fix_args(arguments, patargs, msgargs, custom)); }; fix_delegate.sub = function fix_sub() { return self.sub.apply(this, intern.fix_args(arguments, patargs, msgargs, custom)); }; return fix_delegate; } function options(options, chain) { const self = this; const private$ = self.private$; if (null == options) { return private$.optioner.get(); } // self.log may not exist yet as .options() used during construction if (self.log) { self.log.debug({ kind: 'options', case: 'SET', data: options, }); } let out_opts = (private$.exports.options = private$.optioner.set(options)); if ('string' === typeof options.tag) { const oldtag = self.root.tag; self.root.tag = options.tag; self.root.id = self.root.id.substring(0, self.root.id.indexOf('/' + oldtag)) + '/' + options.tag; } // Update logging configuration if (options.log) { const logspec = private$.logging.build_log(self); out_opts = private$.exports.options = private$.optioner.set({ log: logspec, }); } // Update callpoint if (out_opts.debug.callpoint) { private$.callpoint = (0, common_1.make_callpoint)(out_opts.debug.callpoint); } // DEPRECATED if (out_opts.legacy.logging) { if (options && options.log && Array.isArray(options.log.map)) { for (let i = 0; i < options.log.map.length; ++i) { self.logroute(options.log.map[i]); } } } // TODO: in 4.x, when given options, it should chain // Allow chaining with seneca.options({...}, true) // see https://github.com/rjrodger/seneca/issues/80 return chain ? self : out_opts; } // close seneca instance // sets public seneca.closed property function close(callpoint) { return function api_close(done) { const seneca = this; if (false !== done && null == done) { return (0, common_1.promiser)(intern.close.bind(seneca, callpoint)); } return intern.close.call(seneca, callpoint, done); }; } // Describe this instance using the form: Seneca/VERSION/ID function toString() { return this.fullname; } function seneca() { // Return self. Mostly useful as a check that this is a Seneca instance. return this; } function explain(toggle) { if (true === toggle) { return (this.private$.explain = []); } else if (false === toggle) { const out = this.private$.explain; delete this.private$.explain; return out; } } // Create a Seneca Error, OR set a global error handler function function error(first) { if ('function' === typeof first) { this.options({ errhandler: first }); return this; } else { if (null == first) { throw this.util.error('no_error_code'); } const plugin_fullname = this.fixedargs && this.fixedargs.plugin$ && this.fixedargs.plugin$.full; const plugin = null != plugin_fullname ? this.private$.plugins[plugin_fullname] : this.context.plugin; let err = null; if (plugin && plugin.eraro && plugin.eraro.has(first)) { err = plugin.eraro.apply(this, arguments); } else { err = common_1.error.apply(this, arguments); } return err; } } // NOTE: plugin error codes are in their own namespaces function fail(...args) { if (args.length <= 2) { return failIf(this, true, args[0], args[1]); } if (args.length === 3) { return failIf(this, args[0], args[1], args[2]); } throw this.util.error('fail_wrong_number_of_args', { num_args: args.length }); function failIf(self, cond, code, args) { if (typeof cond !== 'boolean') { throw self.util.error('fail_cond_must_be_bool'); } if (!cond) { return; } const error = self.error(code, args); if (args && false === args.throw$) { return error; } else { throw error; } } } function inward() { const args = Argu(arguments, { inward: Function }); this.root.order.inward.add(args.inward); return this; } function outward() { const args = Argu(arguments, { outward: Function }); this.root.order.outward.add(args.outward); return this; } // TODO: rename fixedargs function delegate(fixedargs, fixedmeta) { const self = this; const root = this.root; const opts = this.options(); fixedargs = fixedargs || {}; fixedmeta = fixedmeta || {}; const delegate = Object.create(self); delegate.private$ = Object.create(self.private$); delegate.did = (delegate.did ? delegate.did + '/' : '') + self.private$.didnid(); function delegate_log() { return root.log.apply(delegate, arguments); } Object.assign(delegate_log, root.log); delegate_log.self = () => delegate; let strdesc; function delegate_toString() { if (strdesc) return strdesc; const vfa = {}; Object.keys(fixedargs).forEach((k) => { const v = fixedargs[k]; if (~k.indexOf('$')) return; vfa[k] = v; }); strdesc = self.toString() + (Object.keys(vfa).length ? '/' + (0, common_1.jsonic_stringify)(vfa) : ''); return strdesc; } const delegate_fixedargs = opts.strict.fixedargs ? Object.assign({}, fixedargs, self.fixedargs) : Object.assign({}, self.fixedargs, fixedargs); const delegate_fixedmeta = opts.strict.fixedmeta ? Object.assign({}, fixedmeta, self.fixedmeta) : Object.assign({}, self.fixedmeta, fixedmeta); function delegate_delegate(further_fixedargs, further_fixedmeta) { const args = Object.assign({}, delegate.fixedargs, further_fixedargs || {}); const meta = Object.assign({}, delegate.fixedmeta, further_fixedmeta || {}); return self.delegate.call(this, args, meta); } // Somewhere to put contextual data for this delegate. // For example, data for individual web requests. const delegate_context = Object.assign({}, self.context); // Prevents incorrect prototype properties in mocha test contexts Object.defineProperties(delegate, { log: { value: delegate_log, writable: true }, toString: { value: delegate_toString, writable: true }, fixedargs: { value: delegate_fixedargs, writable: true }, fixedmeta: { value: delegate_fixedmeta, writable: true }, delegate: { value: delegate_delegate, writable: true }, context: { value: delegate_context, writable: true }, }); return delegate; } // TODO: should be a configuration param so we can handle plugin name resolution function depends() { const self = this; const private$ = this.private$; const error = this.util.error; const args = Argu(arguments, { pluginname: String, deps: (0, gubu_1.Skip)([String]), moredeps: (0, gubu_1.Rest)(String), }); const deps = args.deps || args.moredeps || []; for (let i = 0; i < deps.length; i++) { const depname = deps[i]; if (!private$.plugin_order.byname.includes(depname) && !private$.plugin_order.byname.includes('seneca-' + depname)) { self.die(error('plugin_required', { name: args.pluginname, dependency: depname, })); break; } } } function export$(key) { const self = this; const private$ = this.private$; const error = this.util.error; const opts = this.options(); // Legacy aliases if (key === 'util') { key = 'basic'; } const exportval = private$.exports[key]; if (!exportval && opts.strict.exports) { return self.die(error('export_not_found', { key: key })); } return exportval; } function quiet(flags) { flags = flags || {}; const quiet_opts = { test: false, quiet: true, log: 'none', reload$: true, // TODO: obsolete? }; const opts = this.options(quiet_opts); // An override from env or args is possible. // Only flip to test mode if called from test() method if (opts.test && 'test' !== flags.from) { return this.test(); } else { this.private$.logging.build_log(this); return this; } } function test(errhandler, logspec) { const opts = this.options(); if ('-' != opts.tag) { this.root.id = null == opts.id$ ? this.private$.actnid().substring(0, 4) + '/' + opts.tag : '' + opts.id$; } if ('function' !== typeof errhandler && null !== errhandler) { logspec = errhandler; errhandler = null; } logspec = true === logspec || 'true' === logspec ? 'test' : logspec; const test_opts = { errhandler: null == errhandler ? null : errhandler, test: true, quiet: false, reload$: true, // TODO: obsolete? log: logspec || 'test', debug: { callpoint: true }, }; const set_opts = this.options(test_opts); // An override from env or args is possible. if (set_opts.quiet) { return this.quiet({ from: 'test' }); } else { this.private$.logging.build_log(this); // Manually set logger to test_logger (avoids infecting options structure), // unless there was an external logger defined by the options if (!this.private$.logger.from_options$) { this.root.private$.logger = this.private$.logging.test_logger; } return this; } } function ping() { const now = Date.now(); return { now: now, uptime: now - this.private$.stats.start, id: this.id, cpu: process.cpuUsage(), mem: process.memoryUsage(), act: this.private$.stats.act, tr: this.private$.transport.register.map(function (x) { return Object.assign({ when: x.when, err: x.err }, x.config); }), }; } function translate(from_in, to_in, pick_in, flags) { const from = 'string' === typeof from_in ? this.util.Jsonic(from_in) : from_in; const to = 'string' === typeof to_in ? this.util.Jsonic(to_in) : to_in; let pick = {}; if ('string' === typeof pick_in) { pick_in = pick_in.split(/\s*,\s*/); } if (Array.isArray(pick_in)) { pick_in.forEach(function (prop) { if (prop.startsWith('-')) { pick[prop.substring(1)] = false; } else { pick[prop] = true; } }); } else if (pick_in && 'object' === typeof pick_in) { pick = Object.assign({}, pick_in); } else { pick = null; } let translate = function (msg) { let pick_msg; if (pick) { pick_msg = {}; Object.keys(pick).forEach(function (prop) { if (pick[prop]) { pick_msg[prop] = msg[prop]; } }); } else { pick_msg = (0, common_1.clean)(msg); } let transmsg = Object.assign(pick_msg, to); for (let pn in transmsg) { if (null == transmsg[pn]) { delete transmsg[pn]; } } return transmsg; }; this.private$.translationrouter.add(from, translate); let translation_action = function (msg, reply) { let transmsg = translate(msg); this.act(transmsg, reply); }; Object.defineProperty(translation_action, 'name', { value: 'translation__' + (0, common_1.jsonic_stringify)(from) + '__' + (0, common_1.jsonic_stringify)(to) }); from.translate$ = false; this.add(from, translation_action); return this; } function gate() { return this.delegate({ gate$: true }); } function ungate() { this.fixedargs.gate$ = false; return this; } // TODO this needs a better name function list_plugins() { return Object.assign({}, this.private$.plugins); } function find_plugin(plugindesc, tag) { const plugin_key = (0, common_1.make_plugin_key)(plugindesc, tag); return this.private$.plugins[plugin_key]; } function has_plugin(plugindesc, tag) { const plugin_key = (0, common_1.make_plugin_key)(plugindesc, tag); return !!this.private$.plugins[plugin_key]; } function ignore_plugin(plugindesc, tag, ignore) { if ('boolean' === typeof tag) { ignore = tag; tag = null; } const plugin_key = (0, common_1.make_plugin_key)(plugindesc, tag); const resolved_ignore = (this.private$.ignore_plugins[plugin_key] = null == ignore ? true : !!ignore); this.log.info({ kind: 'plugin', case: 'ignore', full: plugin_key, ignore: resolved_ignore, }); return this; } // Find the action metadata for a given pattern, if it exists. function find(pattern, flags) { const seneca = this; let pat = 'string' === typeof pattern ? seneca.util.Jsonic(pattern) : pattern; pat = seneca.util.clean(pat); pat = pat || {}; let actdef = seneca.private$.actrouter.find(pat, flags && flags.exact); if (!actdef) { actdef = seneca.private$.actrouter.find({}); } return actdef; } // True if an action matching the pattern exists. function has(pattern) { return !!this.find(pattern, { exact: true }); } // List all actions that match the pattern. function list(pattern) { return this.private$.actrouter .list(null == pattern ? {} : this.util.Jsonic(pattern)) .map((x) => x.match); } // Get the current status of the instance. function status(flags) { flags = flags || {}; const hist = this.private$.history.stats(); hist.log = this.private$.history.list(); const status = { stats: this.stats(flags.stats), history: hist, transport: this.private$.transport, }; return status; } // Reply to an action that is waiting for a result. // Used by transports to decouple sending messages from receiving responses. function reply(spec) { const instance = this; let actctxt = null; if (spec && spec.meta) { actctxt = instance.private$.history.get(spec.meta.id); if (actctxt) { actctxt.reply(spec.err, spec.out, spec.meta); } } return !!actctxt; } // Listen for inbound messages. function listen(callpoint) { return function api_listen(...argsarr) { const private$ = this.private$; const self = this; let done = argsarr[argsarr.length - 1]; if (typeof done === 'function') { argsarr.pop(); } else { done = () => { }; } self.log.info({ kind: 'listen', case: 'INIT', data: argsarr, callpoint: callpoint(true), }); const opts = self.options().transport || {}; const config = intern.resolve_config(intern.parse_config(argsarr), opts); self.act('role:transport,cmd:listen', { config: config, gate$: true }, function (err, result) { if (err) { return self.die(private$.error(err, 'transport_listen', config)); } done(null, result); done = () => { }; }); return self; }; } // Send outbound messages. function client(callpoint) { return function api_client() { const private$ = this.private$; const argsarr = Array.prototype.slice.call(arguments); const self = this; self.log.info({ kind: 'client', case: 'INIT', data: argsarr, callpoint: callpoint(true), }); const legacy = self.options().legacy || {}; const opts = self.options().transport || {}; const raw_config = intern.parse_config(argsarr); // pg: pin group raw_config.pg = (0, common_1.pincanon)(raw_config.pin || raw_config.pins); const config = intern.resolve_config(raw_config, opts); config.id = config.id || (0, common_1.pattern)(raw_config); let pins = config.pins || (Array.isArray(config.pin) ? config.pin : [config.pin || '']); pins = pins.map((pin) => { return 'string' === typeof pin ? self.util.Jsonic(pin) : pin; }); // TODO: review - this feels like a hack // perhaps we should instantiate a virtual plugin to represent the client? // ... but is this necessary at all? const task_res = self.order.plugin.task.delegate.exec({ ctx: { seneca: self, }, data: { plugin: { // TODO: make this unique with a counter name: 'seneca_internal_client', tag: void 0, }, }, }); const sd = task_res.out.delegate; let sendclient; const transport_client = function transport_client(msg, reply, meta) { if (legacy.meta) { meta = meta || msg.meta$; } // Undefined plugin init actions pass through here when // there's a catchall client, as they have local$:true if (meta.local) { this.prior(msg, reply); } else if (sendclient && sendclient.send) { if (legacy.meta) { msg.meta$ = meta; } sendclient.send.call(this, msg, reply, meta); } else { this.log.error('no-transport-client', { config: config, msg: msg }); } }; transport_client.id = config.id; if (config.makehandle) { transport_client.handle = config.makehandle(config); } pins.forEach((pin) => { pin = Object.assign({}, pin); // Override local actions, including those more specific than // the client pattern if (config.override) { sd.wrap(sd.util.clean(pin), { client_pattern: sd.util.pattern(pin) }, transport_client); } pin.client$ = true; pin.strict$ = { add: true }; sd.add(pin, transport_client); }); // Create client. sd.act('role:transport,cmd:client', { config: config, gate$: true }, function (err, liveclient) { if (err) { return sd.die(private$.error(err, 'transport_client', config)); } if (null == liveclient) { return sd.die(private$.error('transport_client_null', (0, common_1.clean)(config))); } sendclient = liveclient; }); return self; }; } function decorate() { let args = Argu(arguments, { property: (0, gubu_1.Check)(/^[^_]/, String) .Fault('Decorate property cannot start with underscore (was $VALUE)'), value: (0, gubu_1.Any)() }); let property = args.property; if (this.private$.decorations[property]) { throw new Error('seneca: Decoration already exists: ' + property); } if (this.root[property]) { throw new Error('seneca: Decoration overrides core property:' + property); } this.root[property] = this.private$.decorations[property] = args.value; } intern.parse_config = function (args) { let out = {}; const config = args.filter((x) => null != x); const arglen = config.length; // TODO: use Joi for better error msgs if (arglen === 1) { if (config[0] && 'object' === typeof config[0]) { out = Object.assign({}, config[0]); } else { out.port = parseInt(config[0], 10); } } else if (arglen === 2) { out.port = parseInt(config[0], 10); out.host = config[1]; } else if (arglen === 3) { out.port = parseInt(config[0], 10); out.host = config[1]; out.path = config[2]; } return out; }; intern.resolve_config = function (config, options) { let out = Object.assign({}, config); Object.keys(options).forEach((key) => { const value = options[key]; if (value && 'object' === typeof value) { return; } out[key] = out[key] === void 0 ? value : out[key]; }); // Default transport is web out.type = out.type || 'web'; // DEPRECATED: Remove in 4.0 if (out.type === 'direct' || out.type === 'http') { out.type = 'web'; } const base = options[out.type] || {}; out = Object.assign({}, base, out); if (out.type === 'web' || out.type === 'tcp') { out.port = out.port == null ? base.port : out.port; out.host = out.host == null ? base.host : out.host; out.path = out.path == null ? base.path : out.path; } return out; }; intern.close = function (callpoint, done) { const seneca = this; const options = seneca.options(); let done_called = false; const safe_done = function safe_done(err) { if (!done_called && 'function' === typeof done) { done_called = true; return done.call(seneca, err); } }; // don't try to close twice if (seneca.flags.closed) { return safe_done(); } seneca.ready(do_close); const close_timeout = setTimeout(do_close, options.close_delay); function do_close() { clearTimeout(close_timeout); if (seneca.flags.closed) { return safe_done(); } // TODO: remove in 4.x seneca.closed = true; seneca.flags.closed = true; // cleanup process event listeners (0, common_1.each)(options.system.close_signals, function (active, signal) { if (active) { process.removeListener(signal, seneca.private$.exit_close); } }); seneca.log.debug({ kind: 'close', notice: 'start', callpoint: callpoint(true), }); seneca.act('role:seneca,cmd:close,closing$:true', function (err) { seneca.log.debug(errlog(err, { kind: 'close', notice: 'end' })); seneca.removeAllListeners('act-in'); seneca.removeAllListeners('act-out'); seneca.removeAllListeners('act-err'); seneca.removeAllListeners('pin'); seneca.removeAllListeners('after-pin'); seneca.removeAllListeners('ready'); // Seneca 4 variant seneca.removeAllListeners('act-err-4'); seneca.private$.history.close(); if (seneca.private$.status_interval) { clearInterval(seneca.private$.status_interval); } return safe_done(err); }); } return seneca; }; const FixArgu = Argu('fix', { props: (0, gubu_1.One)((0, gubu_1.Empty)(String), Object), moreprops: (0, gubu_1.Skip)(Object), rest: (0, gubu_1.Rest)((0, gubu_1.Any)()), }); // TODO; this should happen inside .add using a directive intern.fix_args = function (origargs, patargs, msgargs, custom) { // const args = parsePattern(this, origargs, { rest: Rest(Any()) }, patargs) // const args = parsePattern(this, origargs, 'rest:.*', patargs) const args = FixArgu(origargs); args.pattern = Object.assign({}, args.moreprops ? args.moreprops : null, 'string' === typeof args.props ? (0, common_1.parse_jsonic)(args.props, 'add_string_pattern_syntax') : args.props, patargs); const fixargs = [args.pattern] .concat({ fixed$: Object.assign({}, msgargs, args.pattern.fixed$), custom$: Object.assign({}, custom, args.pattern.custom$), }) .concat(args.rest); return fixargs; }; let API = { wrap, fix, options, close, toString, seneca, explain, error, fail, inward, outward, delegate, depends, export: export$, quiet, test, ping, translate, gate, ungate, list_plugins, find_plugin, has_plugin, ignore_plugin, find, has, list, status, reply, listen, client, decorate, }; exports.API = API; //# sourceMappingURL=api.js.map