senecajs/seneca

View on GitHub
seneca.js

Summary

Maintainability
D
2 days
Test Coverage
/* Copyright © 2010-2022 Richard Rodger and other contributors, MIT License. */
'use strict';
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// Node API modules.
const Events = require('events');
const Util = require('util');
// External modules.
const GateExecutor = require('gate-executor');
const Jsonic = require('jsonic');
const UsePlugin = require('use-plugin');
const nid_1 = __importDefault(require("nid"));
const Patrun = require('patrun');
const Stats = require('rolling-stats');
const { Ordu } = require('ordu');
const { Gubu, One, Any, Skip, Open } = require('gubu');
const Eraro = require('eraro');
// Deprecated Legacy modules.
const Optioner = require('optioner');
const Joi = require('@hapi/joi');
// Internal modules.
const Common = require('./lib/common');
const { make_logging } = require('./lib/logging');
const { API } = require('./lib/api');
const { make_ready } = require('./lib/ready');
const Act = require('./lib/act');
const Add = require('./lib/add');
const Sub = require('./lib/sub');
const prior_1 = require("./lib/prior");
const plugin_1 = require("./lib/plugin");
const { Inward } = require('./lib/inward');
const { Outward } = require('./lib/outward');
const { Legacy } = require('./lib/legacy');
const { resolve_options } = require('./lib/options');
const Package = require('./package.json');
const { Print } = require('./lib/print');
const { addActions } = require('./lib/actions');
const { transport } = require('./lib/transport');
// Internal data and utilities.
const { error, deep } = Common;
// Seneca options.
const option_defaults = {
    // Tag this Seneca instance, will be appended to instance identifier.
    tag: '-',
    // Standard timeout for actions.
    timeout: 22222,
    // Standard length of identifiers for actions.
    idlen: 12,
    didlen: 4,
    // Manually set instance identifier.
    id$: Skip(String),
    // Register (true) default plugins. Set false to not register when
    // using custom versions.
    default_plugins: Open({
        transport: true,
    }),
    // Test mode. Use for unit testing.
    test: false,
    // Quiet mode. Moves log level to warn. Use for unit testing.
    quiet: false,
    // Default logging specification - see lib/logging.js
    log: Any(make_logging().default_logspec),
    // Custom logger function, optional - see lib/logging.js
    logger: One(Function, Object, String, null),
    // Wait time for plugins to close gracefully.
    death_delay: 11111,
    // LEGACY: remove in 4.x
    deathdelay: 11111,
    // Wait time for actions to complete before shutdown.
    close_delay: 22222,
    // Legacy; specify general error handler
    errhandler: Skip(One(Function, null)),
    // Load options from a file path
    from: Skip(String),
    // Provide a module to base option require loading from
    module: Skip(),
    // Control error handling.
    error: {
        // Control capture of errors for logging.
        capture: {
            // Capture errors in action callbacks (false throws uncaught).
            callback: true,
            // Capture errors in actions and pass to callback (false throws uncaught).
            action: true,
        }
    },
    // Debug settings.
    debug: {
        // Throw (some) errors from seneca.act.
        fragile: false,
        // Fatal errors ... aren't fatal. Not for production!
        undead: false,
        // Print debug info to console
        print: {
            // Print options. Best used via --seneca.print.options.
            options: false,
            // Amount of information to print on fatal error: 'summary', 'full'
            fatal: 'summary',
            // Include environment when printing full crash report.
            // Default: false for security.
            env: false,
            // Regardless of logging, call `console.err` on errors
            err: false,
            // Depth of object inspection
            depth: 2,
        },
        // Trace action caller and place in args.caller$.
        act_caller: false,
        // Shorten all identifiers to 2 characters.
        short_logs: false,
        // Record and log callpoints (calling code locations).
        callpoint: false,
        // Log deprecation warnings
        deprecation: true,
        // Set to array to force artificial argv and ignore process.argv
        argv: One([], null),
        // Set to object to force artificial env and ignore process.env
        env: One({}, null),
        // Length of data description in logs
        datalen: 111,
    },
    // Enforce strict behaviours. Relax when backwards compatibility needed.
    strict: {
        // Action result must be a plain object.
        result: true,
        // Delegate fixedargs override action args.
        fixedargs: true,
        // Adding a pattern overrides existing pattern only if matches exactly.
        add: false,
        // If no action is found and find is false,
        // then no error returned along with empty object
        find: true,
        // Maximum number of times an action can call itself
        maxloop: 11,
        // Exports must exist
        exports: false,
    },
    // Keep a transient time-ordered history of actions submitted
    history: {
        // History log is active.
        active: true,
        // Prune the history. Disable only for debugging.
        prune: true,
        // Prune the history only periodically.
        interval: 100,
    },
    // Action executor tracing. See gate-executor module.
    trace: {
        act: One(Function, false),
        stack: false,
        // Messages that do not match a known pattern
        unknown: One(String, true),
        // Messages that have invalid content
        invalid: false,
    },
    // Action statistics settings. See rolling-stats module.
    stats: {
        size: 1024,
        interval: 60000,
        running: false,
    },
    // Plugin settings
    plugin: {},
    // Plugins to load (will be passed to .use)
    plugins: One({}, [], null),
    // System wide functionality.
    system: {
        // Function to exit the process.
        exit: (...args) => {
            process.exit(...args);
        },
        // Close instance on these signals, if true.
        close_signals: {
            SIGHUP: false,
            SIGTERM: false,
            SIGINT: false,
            SIGBREAK: false,
        },
        plugin: {
            load_once: false,
        },
    },
    // Internal functionality. Reserved for objects and functions only.
    internal: Open({
        // Console printing utilities
        print: {
            // Print to standard out
            log: One(Function, null),
            // Print to standard err
            err: One(Function, null),
        },
    }),
    // Log status at periodic intervals.
    status: {
        interval: 60000,
        // By default, does not run.
        running: false,
    },
    // Shared default transport configuration
    transport: Open({
        // Standard port for messages.
        port: 10101,
        host: Skip(String),
        path: Skip(String),
        protocol: Skip(String),
    }),
    limits: {
        maxparents: 33,
    },
    // Setup event listeners before starting
    events: {},
    // Backwards compatibility settings.
    legacy: One(Boolean, {
        // Add legacy properties
        actdef: false,
        // Action callback must always have signature callback(error, result).
        action_signature: false,
        // Use old error handling.
        error: true,
        // Use old error codes. REMOVE in Seneca 4.x
        error_codes: false,
        // Use old fail method
        fail: false,
        // Logger can be changed by options method.
        logging: false,
        // Add meta$ property to messages.
        meta: false,
        // Remove meta argument in action arguments and callbacks.
        meta_arg_remove: false,
        // Use seneca-transport plugin.
        transport: true,
        // Insert "[TIMEOUT]" into timeout error message
        timeout_string: true,
        // If false, use Gubu for message validation.
        rules: false,
        // If false, use Gubu for option validation (including plugin defaults)
        options: true,
    }),
    // Processing task ordering.
    order: {
        // Action add task ordering.
        add: {
            // Print task execution log.
            debug: false,
        },
        // Action inward task ordering.
        inward: {
            // Print task execution log.
            debug: false,
        },
        // Action outward task ordering.
        outward: {
            // Print task execution log.
            debug: false,
        },
        // Plugin load task ordering.
        use: {
            // Print task execution log.
            debug: false,
        },
    },
    // Prior actions.
    prior: {
        // Call prior actions directly (not as further messages).
        direct: false,
    },
    // Legacy
    reload$: Skip(Boolean),
    actcache: Any(),
    seneca: Any(),
};
// Utility functions exposed by Seneca via `seneca.util`.
const seneca_util = {
    Eraro: Eraro,
    Jsonic: Jsonic,
    Nid: nid_1.default,
    Patrun: Patrun,
    clean: Common.clean,
    pattern: Common.pattern,
    print: Common.print,
    error: error,
    deep: Common.deep,
    // TODO: expose directly for better DX - no need to namespace under gubu
    // Expose Gubu schema builders (Required, etc.).
    Gubu,
    // Deprecated Legacy (make internal or rename)
    Optioner: Optioner,
    Joi: Joi,
    deepextend: Common.deep,
    parsepattern: Common.parsePattern,
    pincanon: Common.pincanon,
    router: function router() {
        return Patrun();
    },
    resolve_option: Common.resolve_option,
    // Legacy (deprecate and remove)
    argprops: Legacy.argprops,
    recurse: Legacy.recurse,
    copydata: Legacy.copydata,
    nil: Legacy.nil,
    flatten: Legacy.flatten,
};
// Internal implementations.
const intern = {
    util: seneca_util,
};
// Seneca is an EventEmitter.
function Seneca() {
    Events.EventEmitter.call(this);
    this.setMaxListeners(0);
}
Util.inherits(Seneca, Events.EventEmitter);
// Mark the Seneca object
Seneca.prototype.isSeneca = true;
// Provide useful description when convered to JSON.
// Cannot be instantiated from JSON.
Seneca.prototype.toJSON = function toJSON() {
    return {
        isSeneca: true,
        id: this.id,
        did: this.did,
        fixedargs: this.fixedargs,
        fixedmeta: this.fixedmeta,
        start_time: this.start_time,
        version: this.version,
    };
};
Seneca.prototype[Util.inspect.custom] = Seneca.prototype.toJSON;
// Create a Seneca instance.
function init(seneca_options, more_options) {
    var initial_opts = 'string' === typeof seneca_options
        ? deep({}, { from: seneca_options }, more_options)
        : deep({}, seneca_options, more_options);
    // Legacy options, remove in 4.x
    initial_opts.deathdelay = initial_opts.death_delay;
    var seneca = make_seneca(initial_opts);
    var options = seneca.options();
    // The 'internal' key of options is reserved for objects and functions
    // that provide functionality, and are thus not really printable
    seneca.log.debug({ kind: 'notice', options: { ...options, internal: null } });
    Print.print_options(seneca, options);
    // Register default plugins, unless turned off by options.
    if (options.legacy.transport && options.default_plugins.transport) {
        seneca.use(require('seneca-transport'));
    }
    // Register plugins specified in options.
    options.plugins = null == options.plugins ? {} : options.plugins;
    var pluginkeys = Object.keys(options.plugins);
    for (var pkI = 0; pkI < pluginkeys.length; pkI++) {
        var pluginkey = pluginkeys[pkI];
        var plugindesc = options.plugins[pluginkey];
        if (false === plugindesc) {
            seneca.private$.ignore_plugins[pluginkey] = true;
        }
        else {
            seneca.use(plugindesc);
        }
    }
    seneca.ready(function () {
        this.log.info({ kind: 'notice', data: 'hello ' + this.id });
    });
    return seneca;
}
module.exports = init;
// Expose Seneca prototype for easier monkey-patching
module.exports.Seneca = Seneca;
exports.default = init;
// To reference builtin loggers when defining logging options.
module.exports.loghandler = Legacy.loghandler;
// Makes require('seneca').use(...) work by creating an on-the-fly instance.
module.exports.use = function top_use() {
    var argsarr = new Array(arguments.length);
    for (var l = 0; l < argsarr.length; ++l) {
        argsarr[l] = arguments[l];
    }
    var instance = module.exports();
    return instance.use.apply(instance, argsarr);
};
// Makes require('seneca').test() work.
module.exports.test = function top_test() {
    return module.exports().test(...arguments);
};
// Makes require('seneca').quiet() work.
module.exports.quiet = function top_quiet() {
    return module.exports().quiet(...arguments);
};
module.exports.util = seneca_util;
module.exports.valid = Gubu;
module.exports.test$ = { intern: intern };
// Create a new Seneca instance.
function make_seneca(initial_opts) {
    // Create a private context.
    var private$ = make_private();
    private$.error = error;
    // Create a new root Seneca instance.
    var root$ = new Seneca();
    // Expose private data to plugins.
    root$.private$ = private$;
    // Resolve initial options.
    private$.optioner = resolve_options(module, option_defaults, initial_opts);
    var start_opts = private$.optioner.get();
    // Console print utilities
    private$.print = {
        log: start_opts.internal.print.log || Print.internal_log,
        err: start_opts.internal.print.err || Print.internal_err,
    };
    // These need to come from options as required during construction.
    private$.actrouter = start_opts.internal.actrouter || Patrun({ gex: true });
    var soi_subrouter = start_opts.internal.subrouter || {};
    private$.subrouter = {
        // Check for legacy inward router
        inward: soi_subrouter.inward || Patrun({ gex: true }),
        outward: soi_subrouter.outward || Patrun({ gex: true }),
    };
    // Setup event handlers, if defined
    var event_names = ['log', 'act_in', 'act_out', 'act_err', 'ready', 'close'];
    event_names.forEach(function (event_name) {
        if ('function' === typeof start_opts.events[event_name]) {
            root$.on(event_name, start_opts.events[event_name]);
        }
    });
    // Create internal tools.
    private$.actnid = (0, nid_1.default)({ length: start_opts.idlen });
    private$.didnid = (0, nid_1.default)({ length: start_opts.didlen });
    // Instance specific incrementing counters to create unique function names
    private$.next_action_id = Common.autoincr();
    var callpoint = (private$.callpoint = Common.make_callpoint(start_opts.debug.callpoint));
    // Define public member variables.
    root$.start_time = Date.now();
    root$.context = {};
    root$.version = Package.version;
    // TODO: rename in 4.x as "args" terminology is legacy
    root$.fixedargs = {};
    root$.flags = {
        closed: false,
    };
    Object.defineProperty(root$, 'root', { value: root$ });
    private$.history = Common.history(start_opts.history);
    const ready = make_ready(root$);
    // API for Ordu-defined processes.
    root$.order = {};
    // TODO: rename back to plugins
    const api_use = plugin_1.Plugin.api_use(callpoint, {
        debug: !!start_opts.debug.ordu || !!start_opts.order.use.debug,
    });
    root$.use = api_use.use; // Define and load a plugin.
    root$.order.plugin = api_use.ordu;
    // Seneca methods. Official API.
    root$.toString = API.toString;
    root$.has = API.has; // True if the given pattern has an action.
    root$.find = API.find; // Find the action definition for a pattern.
    root$.list = API.list; // List the patterns added to this instance.
    root$.status = API.status; // Get the status if this instance.
    root$.reply = API.reply; // Reply to a submitted message.
    root$.sub = Sub.api_sub; // Subscribe to messages.
    root$.list_plugins = API.list_plugins; // List the registered plugins.
    root$.find_plugin = API.find_plugin; // Find the plugin definition.
    root$.has_plugin = API.has_plugin; // True if the plugin is registered.
    root$.ignore_plugin = API.ignore_plugin; // Ignore plugin and don't register it.
    root$.listen = API.listen(callpoint); // Listen for inbound messages.
    root$.client = API.client(callpoint); // Send outbound messages.
    root$.gate = API.gate; // Create a delegate that executes actions in sequence.
    root$.ungate = API.ungate; // Execute actions in parallel.
    root$.translate = API.translate; // Translate message to new pattern.
    root$.ping = API.ping; // Generate ping response.
    root$.test = API.test; // Set test mode.
    root$.quiet = API.quiet; // Convenience method to set logging level to `warn+`.
    root$.export = API.export; // Export plain objects from a plugin.
    root$.depends = API.depends; // Check for plugin dependencies.
    root$.delegate = API.delegate; // Create an action-specific Seneca instance.
    root$.prior = prior_1.Prior.api_prior; // Call the previous action definition for pattern.
    root$.inward = API.inward; // Add a modifier function for messages inward
    root$.outward = API.outward; // Add a modifier function for responses outward
    root$.error = API.error; // Set global error handler, or generate Seneca Error
    root$.fail = start_opts.legacy.fail
        ? Legacy.make_legacy_fail(start_opts)
        : API.fail; // Throw a Seneca error
    root$.explain = API.explain; // Toggle top level explain capture
    root$.decorate = API.decorate; // Decorate seneca object with functions
    root$.seneca = API.seneca;
    root$.close = API.close(callpoint); // Close and shutdown plugins.
    root$.options = API.options; // Get and set options.
    root$.fix = API.fix; // fix pattern arguments, message arguments, and custom meta
    root$.wrap = API.wrap; // wrap each found pattern with a new action
    root$.add = Add.api_add; // Add a pattern an associated action.
    root$.act = Act.api_act; // Submit a message and trigger the associated action.
    root$.ready = ready.api_ready; // Callback when plugins initialized.
    root$.valid = Gubu; // Expose Gubu shape builders
    root$.internal = function () {
        return {
            ordu: {
                use: api_use.ordu,
            },
        };
    };
    // Non-API methods.
    // root$.register = Plugins.register(callpoint)
    // DEPRECATE IN 4.x
    root$.findact = root$.find;
    root$.plugins = API.list_plugins;
    root$.findplugin = API.find_plugin;
    root$.hasplugin = API.has_plugin;
    root$.hasact = Legacy.hasact;
    root$.act_if = Legacy.act_if;
    root$.findpins = Legacy.findpins;
    root$.pinact = Legacy.findpins;
    root$.next_act = Legacy.next_act;
    // Identifier generator.
    root$.idgen = (0, nid_1.default)({ length: start_opts.idlen });
    // Instance tag
    start_opts.tag = null != start_opts.tag ? start_opts.tag : option_defaults.tag;
    // Create a unique identifer for this instance.
    root$.id =
        start_opts.id$ ||
            root$.idgen() +
                '/' +
                root$.start_time +
                '/' +
                process.pid +
                '/' +
                root$.version +
                '/' +
                start_opts.tag;
    // The instance tag, useful for grouping instances.
    root$.tag = start_opts.tag;
    if (start_opts.debug.short_logs || start_opts.log.short) {
        start_opts.idlen = 2;
        root$.idgen = (0, nid_1.default)({ length: start_opts.idlen });
        root$.id = root$.idgen() + '/' + start_opts.tag;
    }
    root$.fullname = 'Seneca/' + root$.id;
    root$.die = Common.makedie(root$, {
        type: 'sys',
        plugin: 'seneca',
        tag: root$.version,
        id: root$.id,
        callpoint: callpoint,
    });
    root$.util = seneca_util;
    private$.exports = { options: start_opts };
    private$.decorations = {};
    // Error events are fatal, unless you're undead.  These are not the
    // same as action errors, these are unexpected internal issues.
    root$.on('error', root$.die);
    private$.ge = GateExecutor({
        timeout: start_opts.timeout,
    })
        //.clear(action_queue_clear)
        .clear(ready.clear_ready)
        .start();
    // TODO: this should be a plugin
    // setup status log
    if (start_opts.status.interval > 0 && start_opts.status.running) {
        private$.stats = private$.stats || {};
        private$.status_interval = setInterval(function status() {
            root$.log.info({
                kind: 'status',
                alive: Date.now() - private$.stats.start,
                act: private$.stats.act,
            });
        }, start_opts.status.interval);
    }
    if (start_opts.stats) {
        private$.timestats = new Stats.NamedStats(start_opts.stats.size, start_opts.stats.interval);
        if (start_opts.stats.running) {
            setInterval(function stats() {
                private$.timestats.calculate();
            }, start_opts.stats.interval);
        }
    }
    // private$.plugins = {}
    private$.plugin_order = { byname: [], byref: [] };
    private$.use = UsePlugin({
        prefix: ['seneca-', '@seneca/'],
        module: start_opts.internal.module || module,
        msgprefix: false,
        builtin: '',
        merge_defaults: false,
    });
    // TODO: provide an api to add these
    private$.action_modifiers = [
        function add_rules_from_validate_annotation(actdef) {
            actdef.rules = Object.assign(actdef.rules, deep({}, actdef.func.validate || {}));
        },
    ];
    private$.sub = { handler: null, tracers: [] };
    root$.order.add = new Ordu({
        name: 'add',
        debug: !!start_opts.debug.ordu || !!start_opts.order.add.debug,
    })
        .add(Add.task.prepare)
        .add(Add.task.plugin)
        .add(Add.task.callpoint)
        .add(Add.task.flags)
        .add(Add.task.action)
        .add(Add.task.prior)
        .add(Add.task.rules)
        .add(Add.task.register)
        .add(Add.task.modify);
    root$.order.inward = new Ordu({
        name: 'inward',
        debug: !!start_opts.debug.ordu || !!start_opts.order.inward.debug,
    })
        .add(Inward.inward_msg_modify)
        .add(Inward.inward_closed)
        .add(Inward.inward_act_cache)
        .add(Inward.inward_act_default)
        .add(Inward.inward_act_not_found)
        .add(Inward.inward_act_stats)
        .add(Inward.inward_validate_msg)
        .add(Inward.inward_warnings)
        .add(Inward.inward_msg_meta)
        .add(Inward.inward_limit_msg)
        .add(Inward.inward_prepare_delegate)
        .add(Inward.inward_sub)
        .add(Inward.inward_announce);
    root$.order.outward = new Ordu({
        name: 'outward',
        debug: !!start_opts.debug.ordu || !!start_opts.order.outward.debug,
    })
        .add(Outward.outward_make_error)
        .add(Outward.outward_act_stats)
        .add(Outward.outward_act_cache)
        .add(Outward.outward_res_object)
        .add(Outward.outward_res_entity)
        .add(Outward.outward_msg_meta)
        .add(Outward.outward_trace)
        .add(Outward.outward_sub)
        .add(Outward.outward_announce)
        .add(Outward.outward_act_error);
    // Configure logging
    // Mark logger as being externally defined from options
    if (start_opts.logger && 'object' === typeof start_opts.logger) {
        start_opts.logger.from_options$ = true;
    }
    // Load logger and update log options
    var logspec = private$.logging.build_log(root$);
    start_opts = private$.exports.options = private$.optioner.set({
        log: logspec,
    });
    if (start_opts.test) {
        root$.test('string' === typeof start_opts.test ? start_opts.test : null);
    }
    if (start_opts.quiet) {
        root$.quiet();
    }
    private$.exit_close = function () {
        root$.close(function root_exit_close(err) {
            if (err && true != private$.optioner.get().quiet) {
                private$.print.err(err);
            }
            start_opts.system.exit(err ? (err.exit === null ? 1 : err.exit) : 0);
        });
    };
    addActions(root$);
    // root$.act('role:seneca,cmd:pingx')
    if (!start_opts.legacy.transport) {
        start_opts.legacy.error = false;
        // TODO: move to static options in Seneca 4.x
        start_opts.transport = deep({
            port: 62345,
            host: '127.0.0.1',
            path: '/act',
            protocol: 'http',
        }, start_opts.transport);
        transport(root$);
    }
    Print(root$, start_opts.debug.argv || process.argv);
    Common.each(start_opts.system.close_signals, function (active, signal) {
        if (active) {
            process.once(signal, private$.exit_close);
        }
    });
    return root$;
}
// Private member variables of Seneca object.
function make_private() {
    return {
        logging: make_logging(),
        stats: {
            start: Date.now(),
            act: {
                calls: 0,
                done: 0,
                fails: 0,
                cache: 0,
            },
            actmap: {},
        },
        actdef: {},
        transport: {
            register: [],
        },
        plugins: {
            // Virtual "plugin" for top level actions.
            root$: {
                name: 'root$',
                fullname: 'root$',
                tag: '-',
                options: Object.create(null),
                shared: Object.create(null),
            }
        },
        ignore_plugins: {},
    };
}
//# sourceMappingURL=seneca.js.map