lib/inward.ts
/* Copyright © 2010-2023 Richard Rodger and other contributors, MIT License. */
import {
TRACE_PATTERN,
TRACE_ACTION,
clean,
make_trace_desc,
inspect,
} from './common'
const intern: any = {}
function inward_msg_modify(spec: any) {
const ctx = spec.ctx
const data = spec.data
var meta = data.meta
if (ctx.actdef) {
var fixed = ctx.actdef.fixed
var custom = ctx.actdef.custom
if (fixed) {
Object.assign(data.msg, fixed)
}
if (custom) {
meta.custom = meta.custom || {}
Object.assign(meta.custom, custom)
}
}
}
function inward_limit_msg(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var meta = data.meta
if (meta.parents && so.limits.maxparents < meta.parents.length) {
return {
op: 'stop',
out: {
kind: 'error',
code: 'maxparents',
info: {
maxparents: so.limits.maxparents,
numparents: meta.parents.length,
parents: meta.parents.map(
(p: any) => p[TRACE_PATTERN] + ' ' + p[TRACE_ACTION]
),
args: inspect(clean(data.msg)).replace(/\n/g, ''),
},
},
}
}
}
function inward_announce(spec: any) {
const ctx = spec.ctx
const data = spec.data
if (!ctx.actdef) return
// Only intended for use in a per-delegate context.
if ('function' === typeof ctx.seneca.on_act_in) {
ctx.seneca.on_act_in(ctx.actdef, data.msg, data.meta)
}
ctx.seneca.emit('act-in', data.msg, null, data.meta)
}
// TODO: allow if not a top level call - close gracefully
function inward_closed(spec: any) {
const ctx = spec.ctx
const data = spec.data
if (ctx.seneca.flags.closed && !data.meta.closing) {
return {
op: 'stop',
out: {
kind: 'error',
code: 'closed',
info: {
args: inspect(clean(data.msg)).replace(/\n/g, ''),
},
},
}
}
}
function inward_act_stats(spec: any) {
const ctx = spec.ctx
if (!ctx.actdef) {
return
}
var private$ = ctx.seneca.private$
++private$.stats.act.calls
var pattern = ctx.actdef.pattern
var actstats = (private$.stats.actmap[pattern] =
private$.stats.actmap[pattern] || {})
++actstats.calls
}
function inward_act_default(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var msg = data.msg
var meta = data.meta
// TODO: existence of pattern action needs own indicator flag
if (!ctx.actdef) {
var default$ = meta.dflt || (!so.strict.find ? {} : meta.dflt)
if (
null != default$ &&
('object' === typeof default$ || Array.isArray(default$))
) {
return {
op: 'stop',
out: {
kind: 'result',
result: default$,
log: {
level: 'debug',
data: {
kind: 'act',
case: 'DEFAULT',
},
},
},
}
} else if (null != default$) {
return {
op: 'stop',
out: {
kind: 'error',
code: 'act_default_bad',
info: {
args: inspect(clean(msg)).replace(/\n/g, ''),
xdefault: inspect(default$),
},
},
}
}
}
}
function inward_act_not_found(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var msg = data.msg
// console.log('NO ACTDEF', ctx.actdef, msg, data)
if (!ctx.actdef) {
return {
op: 'stop',
out: {
kind: 'error',
code: 'act_not_found',
info: { args: inspect(clean(msg)).replace(/\n/g, '') },
log: {
level: so.trace.unknown ? 'warn' : 'debug',
data: {
kind: 'act',
case: 'UNKNOWN',
},
},
},
}
}
}
function inward_validate_msg(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var msg = data.msg
var err: any = null
if (so.valid.active && so.valid.message) {
if (ctx.actdef.gubu) {
// TODO: gubu option to provide Error without throwing
// TODO: how to expose gubu builders, Required, etc?
// TODO: use original msg for error
try {
data.msg = ctx.actdef.gubu(msg)
} catch (e) {
err = e
}
}
else if ('function' === typeof ctx.actdef.validate) {
// FIX: this is assumed to be synchronous
// seneca-parambulator and seneca-joi need to be updated
ctx.actdef.validate(msg, function(verr: any) {
err = verr
})
}
}
if (err) {
return {
op: 'stop',
out: {
kind: 'error',
code: so.legacy.error_codes ? 'act_invalid_args' : 'act_invalid_msg',
info: {
pattern: ctx.actdef.pattern,
message: err.message,
msg: clean(msg),
error: err,
props: err.gubu ? err.props : [],
},
log: {
level: so.trace.invalid ? 'warn' : null,
data: {
kind: 'act',
case: 'INVALID',
},
},
},
}
}
}
// Check if actid has already been seen, and if action cache is active,
// then provide cached result, if any. Return true in this case.
function inward_act_cache(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var meta = data.meta
var actid = meta.id
var private$ = ctx.seneca.private$
if (actid != null && so.history.active) {
var actdetails = private$.history.get(actid)
if (actdetails) {
private$.stats.act.cache++
var latest = actdetails.result[actdetails.result.length - 1] || {}
var out = {
op: 'stop',
out: {
kind: latest.err ? 'error' : 'result',
result: latest.res || null,
error: latest.err || null,
log: {
level: 'debug',
data: {
kind: 'act',
case: 'CACHE',
cachetime: latest.when,
},
},
},
}
ctx.cached$ = true
return out
}
}
}
function inward_warnings(spec: any) {
const ctx = spec.ctx
const data = spec.data
var so = ctx.options
var msg = data.msg
if (so.debug.deprecation && ctx.actdef.deprecate) {
ctx.seneca.log.warn({
kind: 'act',
case: 'DEPRECATED',
msg: msg,
pattern: ctx.actdef.pattern,
notice: ctx.actdef.deprecate,
callpoint: ctx.callpoint,
})
}
}
function inward_msg_meta(spec: any) {
const ctx = spec.ctx
const data = spec.data
var meta = data.meta
meta.pattern = ctx.actdef.pattern
meta.client_pattern = ctx.actdef.client_pattern
meta.action = ctx.actdef.id
meta.plugin = Object.assign({}, meta.plugin, ctx.actdef.plugin)
meta.start = null == meta.start ? ctx.start : meta.start
meta.parents = meta.parents || []
meta.trace = meta.trace || []
var parent = ctx.seneca.private$.act && ctx.seneca.private$.act.parent
// Use parent custom object if present,
// otherwise use object provided by caller,
// otherwise create a new one.
// This preserves the same custom object ref throughout a call chain.
var parentcustom = (parent && parent.custom) || meta.custom || {}
if (parent) {
meta.parents = meta.parents.concat(parent.parents || [])
meta.parents.unshift(make_trace_desc(parent))
}
meta.custom = Object.assign(
parentcustom,
meta.custom,
ctx.seneca.fixedmeta && ctx.seneca.fixedmeta.custom
)
// meta.explain is an array that explanation objects can be appended to.
// The same array is used through the action call tree, and must be provided by
// calling code at the top level via the explain$ directive.
if (data.msg.explain$ && Array.isArray(data.msg.explain$)) {
meta.explain = data.msg.explain$
} else if (parent && parent.explain) {
meta.explain = parent.explain
}
if (ctx.seneca.private$.explain) {
meta.explain = meta.explain || []
ctx.seneca.private$.explain.push(meta.explain)
}
}
function inward_prepare_delegate(spec: any) {
const ctx = spec.ctx
const data = spec.data
const meta = data.meta
const plugin = ctx.seneca.private$.plugins[meta.plugin.fullname]
if (plugin) {
ctx.seneca.plugin = plugin
ctx.seneca.shared = plugin.shared
}
ctx.seneca.fixedargs.tx$ = data.meta.tx
data.reply = data.reply.bind(ctx.seneca)
data.reply.seneca = ctx.seneca
// const reply = data.reply
// // DEPRECATE
// ctx.seneca.good = function good(out: any) {
// ctx.seneca.log.warn(
// 'seneca.good is deprecated and will be removed in 4.0.0'
// )
// reply(null, out)
// }
// // DEPRECATE
// ctx.seneca.bad = function bad(err: any) {
// ctx.seneca.log.warn('seneca.bad is deprecated and will be removed in 4.0.0')
// reply(err)
// }
ctx.seneca.reply = function reply(err: any, out: any) {
reply(err, out)
}
ctx.seneca.explain = intern.explain.bind(ctx.seneca, meta)
if (meta.explain) {
ctx.seneca.explain({ explain$: true, msg$: clean(data.msg) })
}
}
function inward_sub(spec: any) {
const ctx = spec.ctx
const data = spec.data
var meta = data.meta
var private$ = ctx.seneca.private$
// Only entry msg of prior chain is published
if (meta.prior) {
return
}
var submsg = ctx.seneca.util.clean(data.msg)
// Find all subscription matches, even partial.
// Example: a:1,b:2 matches subs for a:1; a:1,b:1; b:1
var sub_actions_list = private$.subrouter.inward.find(submsg, false, true)
submsg.in$ = true
for (var alI = 0; alI < sub_actions_list.length; alI++) {
var sub_actions = sub_actions_list[alI] // Also an array.
for (var sI = 0; sI < sub_actions.length; sI++) {
var sub_action = sub_actions[sI]
try {
sub_action.call(ctx.seneca, submsg, null, data.meta)
} catch (sub_err) {
// DESIGN: this should be all that is needed.
return {
op: 'stop',
out: {
kind: 'error',
code: 'sub_inward_action_failed',
error: sub_err,
},
}
}
}
}
}
intern.explain = function(meta: any, entry: any) {
var orig_explain = this.explain
var explain = meta.explain
if (true === entry || false === entry) {
return orig_explain.call(this, entry)
} else if (explain) {
if (null != entry) {
if (entry.explain$) {
entry.explain$ = {
start: meta.start,
pattern: meta.pattern,
action: meta.action,
id: meta.id,
instance: meta.instance,
tag: meta.tag,
seneca: meta.seneca,
version: meta.version,
gate: meta.gate,
fatal: meta.fatal,
local: meta.local,
direct: meta.direct,
closing: meta.closing,
timeout: meta.timeout,
dflt: meta.dflt,
custom: meta.custom,
plugin: meta.plugin,
prior: meta.prior,
caller: meta.caller,
parents: meta.parents,
remote: meta.remote,
sync: meta.sync,
trace: meta.trace,
sub: meta.sub,
data: meta.data,
err: meta.err,
err_trace: meta.err_trace,
error: meta.error,
empty: meta.empty,
}
}
explain.push(
entry && 'object' === typeof entry ? entry : { content: entry }
)
}
}
return explain && this.explain
}
let Inward = {
inward_msg_modify,
inward_closed,
inward_act_cache,
inward_act_default,
inward_act_not_found,
inward_validate_msg,
inward_warnings,
inward_msg_meta,
inward_limit_msg,
inward_act_stats,
inward_prepare_delegate,
inward_announce,
inward_sub,
intern,
}
export { Inward }