diff --git a/src/backend/src/modules/core/ContextService.js b/src/backend/src/modules/core/ContextService.js new file mode 100644 index 00000000..d8775b8f --- /dev/null +++ b/src/backend/src/modules/core/ContextService.js @@ -0,0 +1,21 @@ +const BaseService = require("../../services/BaseService"); +const { Context } = require("../../util/context"); + +/** + * ContextService provides a way for other services to register a hook to be + * called when a context/subcontext is created. + * + * Contexts are used to provide contextual information in the execution + * context (dynamic scope). They can also be used to identify a "span"; + * a span is a labelled frame of execution that can be used to track + * performance, errors, and other metrics. + */ +class ContextService extends BaseService { + register_context_hook (event, hook) { + Context.context_hooks_[event].push(hook); + } +} + +module.exports = { + ContextService, +}; diff --git a/src/backend/src/modules/core/Core2Module.js b/src/backend/src/modules/core/Core2Module.js index 7ee16a8c..d3a79efb 100644 --- a/src/backend/src/modules/core/Core2Module.js +++ b/src/backend/src/modules/core/Core2Module.js @@ -50,6 +50,9 @@ class Core2Module extends AdvancedBase { const { ParameterService } = require("./ParameterService.js"); services.registerService('params', ParameterService); + + const { ContextService } = require('./ContextService.js'); + services.registerService('context', ContextService); } } diff --git a/src/backend/src/util/context.js b/src/backend/src/util/context.js index 7089419d..3430e15c 100644 --- a/src/backend/src/util/context.js +++ b/src/backend/src/util/context.js @@ -23,6 +23,13 @@ class Context { static USE_NAME_FALLBACK = {}; static next_name_ = 0; static other_next_names_ = {}; + + // Context hooks should be registered via service (ContextService.js) + static context_hooks_ = { + pre_create: [], + post_create: [], + pre_arun: [], + }; static contextAsyncLocalStorage = new AsyncLocalStorage(); static __last_context_key = 0; @@ -59,8 +66,8 @@ class Context { static describe () { return this.get().describe(); } - static arun (cb) { - return this.get().arun(cb); + static arun (...a) { + return this.get().arun(...a); } static sub (values, opt_name) { return this.get().sub(values, opt_name); @@ -72,6 +79,14 @@ class Context { this.values_[k] = v; } sub (values, opt_name) { + if ( typeof values === 'string' ) { + opt_name = values; + values = {}; + } + const name = opt_name ?? this.name ?? this.get('name'); + for ( const hook of this.constructor.context_hooks_.pre_create ) { + hook({ values, name }); + } return new Context(values, this, opt_name); } get values () { @@ -99,6 +114,7 @@ class Context { opt_parent = opt_parent || Context.root; + this.trace_name = opt_name ?? undefined; this.name = (() => { if ( opt_name === this.constructor.USE_NAME_FALLBACK ) { opt_name = 'F'; @@ -129,7 +145,34 @@ class Context { this.values_ = values; } - async arun (cb) { + async arun (...args) { + let cb = args.shift(); + + let hints = {}; + if ( typeof cb === 'object' ) { + hints = cb; + cb = args.shift(); + } + + if ( typeof cb === 'string' ) { + const sub_context = this.sub(cb); + return await sub_context.arun({ trace: true }, ...args); + } + + const replace_callback = new_cb => { + cb = new_cb; + } + + for ( const hook of this.constructor.context_hooks_.pre_arun ) { + hook({ + hints, + name: this.name ?? this.get('name'), + trace_name: this.trace_name, + replace_callback, + callback: cb, + }); + } + const als = this.constructor.contextAsyncLocalStorage; return await als.run(new Map(), async () => { als.getStore().set('context', this);