UNPKG

33.2 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
2// Node module: @loopback/context
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import debugFactory, {Debugger} from 'debug';
7import {EventEmitter} from 'events';
8import {
9 Binding,
10 BindingInspectOptions,
11 BindingScope,
12 BindingTag,
13} from './binding';
14import {
15 ConfigurationResolver,
16 DefaultConfigurationResolver,
17} from './binding-config';
18import {
19 BindingFilter,
20 filterByKey,
21 filterByTag,
22 isBindingTagFilter,
23} from './binding-filter';
24import {BindingAddress, BindingKey} from './binding-key';
25import {BindingComparator} from './binding-sorter';
26import {ContextEvent, ContextEventListener} from './context-event';
27import {ContextEventObserver, ContextObserver} from './context-observer';
28import {ContextSubscriptionManager, Subscription} from './context-subscription';
29import {ContextTagIndexer} from './context-tag-indexer';
30import {ContextView} from './context-view';
31import {JSONObject} from './json-types';
32import {ContextBindings} from './keys';
33import {
34 asResolutionOptions,
35 ResolutionError,
36 ResolutionOptions,
37 ResolutionOptionsOrSession,
38 ResolutionSession,
39} from './resolution-session';
40import {generateUniqueId} from './unique-id';
41import {
42 BoundValue,
43 Constructor,
44 getDeepProperty,
45 isPromiseLike,
46 transformValueOrPromise,
47 ValueOrPromise,
48} from './value-promise';
49
50/**
51 * Context provides an implementation of Inversion of Control (IoC) container
52 */
53export class Context extends EventEmitter {
54 /**
55 * Name of the context
56 */
57 readonly name: string;
58
59 /**
60 * Key to binding map as the internal registry
61 */
62 protected readonly registry: Map<string, Binding> = new Map();
63
64 /**
65 * Indexer for bindings by tag
66 */
67 protected readonly tagIndexer: ContextTagIndexer;
68
69 /**
70 * Manager for observer subscriptions
71 */
72 readonly subscriptionManager: ContextSubscriptionManager;
73
74 /**
75 * Parent context
76 */
77 protected _parent?: Context;
78
79 /**
80 * Configuration resolver
81 */
82 protected configResolver: ConfigurationResolver;
83
84 /**
85 * A debug function which can be overridden by subclasses.
86 *
87 * @example
88 * ```ts
89 * import debugFactory from 'debug';
90 * const debug = debugFactory('loopback:context:application');
91 * export class Application extends Context {
92 * super('application');
93 * this._debug = debug;
94 * }
95 * ```
96 */
97 protected _debug: Debugger;
98
99 /**
100 * Scope for binding resolution
101 */
102 scope: BindingScope = BindingScope.CONTEXT;
103
104 /**
105 * Create a new context.
106 *
107 * @example
108 * ```ts
109 * // Create a new root context, let the framework to create a unique name
110 * const rootCtx = new Context();
111 *
112 * // Create a new child context inheriting bindings from `rootCtx`
113 * const childCtx = new Context(rootCtx);
114 *
115 * // Create another root context called "application"
116 * const appCtx = new Context('application');
117 *
118 * // Create a new child context called "request" and inheriting bindings
119 * // from `appCtx`
120 * const reqCtx = new Context(appCtx, 'request');
121 * ```
122 * @param _parent - The optional parent context
123 * @param name - Name of the context. If not provided, a unique identifier
124 * will be generated as the name.
125 */
126 constructor(_parent?: Context | string, name?: string) {
127 super();
128 // The number of listeners can grow with the number of child contexts
129 // For example, each request can add a listener to the RestServer and the
130 // listener is removed when the request processing is finished.
131 // See https://github.com/loopbackio/loopback-next/issues/4363
132 this.setMaxListeners(Infinity);
133 if (typeof _parent === 'string') {
134 name = _parent;
135 _parent = undefined;
136 }
137 this._parent = _parent;
138 this.name = name ?? this.generateName();
139 this.tagIndexer = new ContextTagIndexer(this);
140 this.subscriptionManager = new ContextSubscriptionManager(this);
141 this._debug = debugFactory(this.getDebugNamespace());
142 }
143
144 /**
145 * Get the debug namespace for the context class. Subclasses can override
146 * this method to supply its own namespace.
147 *
148 * @example
149 * ```ts
150 * export class Application extends Context {
151 * super('application');
152 * }
153 *
154 * protected getDebugNamespace() {
155 * return 'loopback:context:application';
156 * }
157 * ```
158 */
159 protected getDebugNamespace() {
160 if (this.constructor === Context) return 'loopback:context';
161 const name = this.constructor.name.toLowerCase();
162 return `loopback:context:${name}`;
163 }
164
165 private generateName() {
166 const id = generateUniqueId();
167 if (this.constructor === Context) return id;
168 return `${this.constructor.name}-${id}`;
169 }
170
171 /**
172 * @internal
173 * Getter for ContextSubscriptionManager
174 */
175 get parent() {
176 return this._parent;
177 }
178
179 /**
180 * Wrap the debug statement so that it always print out the context name
181 * as the prefix
182 * @param args - Arguments for the debug
183 */
184 protected debug(...args: unknown[]) {
185 /* istanbul ignore if */
186 if (!this._debug.enabled) return;
187 const formatter = args.shift();
188 if (typeof formatter === 'string') {
189 this._debug(`[%s] ${formatter}`, this.name, ...args);
190 } else {
191 this._debug('[%s] ', this.name, formatter, ...args);
192 }
193 }
194
195 /**
196 * A strongly-typed method to emit context events
197 * @param type Event type
198 * @param event Context event
199 */
200 emitEvent<T extends ContextEvent>(type: string, event: T) {
201 this.emit(type, event);
202 }
203
204 /**
205 * Emit an `error` event
206 * @param err Error
207 */
208 emitError(err: unknown) {
209 this.emit('error', err);
210 }
211
212 /**
213 * Create a binding with the given key in the context. If a locked binding
214 * already exists with the same key, an error will be thrown.
215 *
216 * @param key - Binding key
217 */
218 bind<ValueType = BoundValue>(
219 key: BindingAddress<ValueType>,
220 ): Binding<ValueType> {
221 const binding = new Binding<ValueType>(key.toString());
222 this.add(binding);
223 return binding;
224 }
225
226 /**
227 * Add a binding to the context. If a locked binding already exists with the
228 * same key, an error will be thrown.
229 * @param binding - The configured binding to be added
230 */
231 add(binding: Binding<unknown>): this {
232 const key = binding.key;
233 this.debug('[%s] Adding binding: %s', key);
234 let existingBinding: Binding | undefined;
235 const keyExists = this.registry.has(key);
236 if (keyExists) {
237 existingBinding = this.registry.get(key);
238 const bindingIsLocked = existingBinding?.isLocked;
239 if (bindingIsLocked)
240 throw new Error(`Cannot rebind key "${key}" to a locked binding`);
241 }
242 this.registry.set(key, binding);
243 if (existingBinding !== binding) {
244 if (existingBinding != null) {
245 this.emitEvent('unbind', {
246 binding: existingBinding,
247 context: this,
248 type: 'unbind',
249 });
250 }
251 this.emitEvent('bind', {binding, context: this, type: 'bind'});
252 }
253 return this;
254 }
255
256 /**
257 * Create a corresponding binding for configuration of the target bound by
258 * the given key in the context.
259 *
260 * For example, `ctx.configure('controllers.MyController').to({x: 1})` will
261 * create binding `controllers.MyController:$config` with value `{x: 1}`.
262 *
263 * @param key - The key for the binding to be configured
264 */
265 configure<ConfigValueType = BoundValue>(
266 key: BindingAddress = '',
267 ): Binding<ConfigValueType> {
268 const bindingForConfig = Binding.configure<ConfigValueType>(key);
269 this.add(bindingForConfig);
270 return bindingForConfig;
271 }
272
273 /**
274 * Get the value or promise of configuration for a given binding by key
275 *
276 * @param key - Binding key
277 * @param propertyPath - Property path for the option. For example, `x.y`
278 * requests for `<config>.x.y`. If not set, the `<config>` object will be
279 * returned.
280 * @param resolutionOptions - Options for the resolution.
281 * - optional: if not set or set to `true`, `undefined` will be returned if
282 * no corresponding value is found. Otherwise, an error will be thrown.
283 */
284 getConfigAsValueOrPromise<ConfigValueType>(
285 key: BindingAddress,
286 propertyPath?: string,
287 resolutionOptions?: ResolutionOptions,
288 ): ValueOrPromise<ConfigValueType | undefined> {
289 this.setupConfigurationResolverIfNeeded();
290 return this.configResolver.getConfigAsValueOrPromise(
291 key,
292 propertyPath,
293 resolutionOptions,
294 );
295 }
296
297 /**
298 * Set up the configuration resolver if needed
299 */
300 protected setupConfigurationResolverIfNeeded() {
301 if (!this.configResolver) {
302 // First try the bound ConfigurationResolver to this context
303 const configResolver = this.getSync<ConfigurationResolver>(
304 ContextBindings.CONFIGURATION_RESOLVER,
305 {
306 optional: true,
307 },
308 );
309 if (configResolver) {
310 this.debug(
311 'Custom ConfigurationResolver is loaded from %s.',
312 ContextBindings.CONFIGURATION_RESOLVER.toString(),
313 );
314 this.configResolver = configResolver;
315 } else {
316 // Fallback to DefaultConfigurationResolver
317 this.debug('DefaultConfigurationResolver is used.');
318 this.configResolver = new DefaultConfigurationResolver(this);
319 }
320 }
321 return this.configResolver;
322 }
323
324 /**
325 * Resolve configuration for the binding by key
326 *
327 * @param key - Binding key
328 * @param propertyPath - Property path for the option. For example, `x.y`
329 * requests for `<config>.x.y`. If not set, the `<config>` object will be
330 * returned.
331 * @param resolutionOptions - Options for the resolution.
332 */
333 async getConfig<ConfigValueType>(
334 key: BindingAddress,
335 propertyPath?: string,
336 resolutionOptions?: ResolutionOptions,
337 ): Promise<ConfigValueType | undefined> {
338 return this.getConfigAsValueOrPromise<ConfigValueType>(
339 key,
340 propertyPath,
341 resolutionOptions,
342 );
343 }
344
345 /**
346 * Resolve configuration synchronously for the binding by key
347 *
348 * @param key - Binding key
349 * @param propertyPath - Property path for the option. For example, `x.y`
350 * requests for `config.x.y`. If not set, the `config` object will be
351 * returned.
352 * @param resolutionOptions - Options for the resolution.
353 */
354 getConfigSync<ConfigValueType>(
355 key: BindingAddress,
356 propertyPath?: string,
357 resolutionOptions?: ResolutionOptions,
358 ): ConfigValueType | undefined {
359 const valueOrPromise = this.getConfigAsValueOrPromise<ConfigValueType>(
360 key,
361 propertyPath,
362 resolutionOptions,
363 );
364 if (isPromiseLike(valueOrPromise)) {
365 const prop = propertyPath ? ` property ${propertyPath}` : '';
366 throw new Error(
367 `Cannot get config${prop} for ${key} synchronously: the value is a promise`,
368 );
369 }
370 return valueOrPromise;
371 }
372
373 /**
374 * Unbind a binding from the context. No parent contexts will be checked.
375 *
376 * @remarks
377 * If you need to unbind a binding owned by a parent context, use the code
378 * below:
379 *
380 * ```ts
381 * const ownerCtx = ctx.getOwnerContext(key);
382 * return ownerCtx != null && ownerCtx.unbind(key);
383 * ```
384 *
385 * @param key - Binding key
386 * @returns true if the binding key is found and removed from this context
387 */
388 unbind(key: BindingAddress): boolean {
389 this.debug('Unbind %s', key);
390 key = BindingKey.validate(key);
391 const binding = this.registry.get(key);
392 // If not found, return `false`
393 if (binding == null) return false;
394 if (binding?.isLocked)
395 throw new Error(`Cannot unbind key "${key}" of a locked binding`);
396 this.registry.delete(key);
397 this.emitEvent('unbind', {binding, context: this, type: 'unbind'});
398 return true;
399 }
400
401 /**
402 * Add a context event observer to the context
403 * @param observer - Context observer instance or function
404 */
405 subscribe(observer: ContextEventObserver): Subscription {
406 return this.subscriptionManager.subscribe(observer);
407 }
408
409 /**
410 * Remove the context event observer from the context
411 * @param observer - Context event observer
412 */
413 unsubscribe(observer: ContextEventObserver): boolean {
414 return this.subscriptionManager.unsubscribe(observer);
415 }
416
417 /**
418 * Close the context: clear observers, stop notifications, and remove event
419 * listeners from its parent context.
420 *
421 * @remarks
422 * This method MUST be called to avoid memory leaks once a context object is
423 * no longer needed and should be recycled. An example is the `RequestContext`,
424 * which is created per request.
425 */
426 close() {
427 this.debug('Closing context...');
428 this.subscriptionManager.close();
429 this.tagIndexer.close();
430 }
431
432 /**
433 * Check if an observer is subscribed to this context
434 * @param observer - Context observer
435 */
436 isSubscribed(observer: ContextObserver) {
437 return this.subscriptionManager.isSubscribed(observer);
438 }
439
440 /**
441 * Create a view of the context chain with the given binding filter
442 * @param filter - A function to match bindings
443 * @param comparator - A function to sort matched bindings
444 * @param options - Resolution options
445 */
446 createView<T = unknown>(
447 filter: BindingFilter,
448 comparator?: BindingComparator,
449 options?: Omit<ResolutionOptions, 'session'>,
450 ) {
451 const view = new ContextView<T>(this, filter, comparator, options);
452 view.open();
453 return view;
454 }
455
456 /**
457 * Check if a binding exists with the given key in the local context without
458 * delegating to the parent context
459 * @param key - Binding key
460 */
461 contains(key: BindingAddress): boolean {
462 key = BindingKey.validate(key);
463 return this.registry.has(key);
464 }
465
466 /**
467 * Check if a key is bound in the context or its ancestors
468 * @param key - Binding key
469 */
470 isBound(key: BindingAddress): boolean {
471 if (this.contains(key)) return true;
472 if (this._parent) {
473 return this._parent.isBound(key);
474 }
475 return false;
476 }
477
478 /**
479 * Get the owning context for a binding or its key
480 * @param keyOrBinding - Binding object or key
481 */
482 getOwnerContext(
483 keyOrBinding: BindingAddress | Readonly<Binding<unknown>>,
484 ): Context | undefined {
485 let key: BindingAddress;
486 if (keyOrBinding instanceof Binding) {
487 key = keyOrBinding.key;
488 } else {
489 key = keyOrBinding as BindingAddress;
490 }
491 if (this.contains(key)) {
492 if (keyOrBinding instanceof Binding) {
493 // Check if the contained binding is the same
494 if (this.registry.get(key.toString()) === keyOrBinding) {
495 return this;
496 }
497 return undefined;
498 }
499 return this;
500 }
501 if (this._parent) {
502 return this._parent.getOwnerContext(key);
503 }
504 return undefined;
505 }
506
507 /**
508 * Get the context matching the scope
509 * @param scope - Binding scope
510 */
511 getScopedContext(
512 scope:
513 | BindingScope.APPLICATION
514 | BindingScope.SERVER
515 | BindingScope.REQUEST,
516 ): Context | undefined {
517 if (this.scope === scope) return this;
518 if (this._parent) {
519 return this._parent.getScopedContext(scope);
520 }
521 return undefined;
522 }
523
524 /**
525 * Locate the resolution context for the given binding. Only bindings in the
526 * resolution context and its ancestors are visible as dependencies to resolve
527 * the given binding
528 * @param binding - Binding object
529 */
530 getResolutionContext(
531 binding: Readonly<Binding<unknown>>,
532 ): Context | undefined {
533 let resolutionCtx: Context | undefined;
534 switch (binding.scope) {
535 case BindingScope.SINGLETON:
536 // Use the owner context
537 return this.getOwnerContext(binding.key);
538 case BindingScope.TRANSIENT:
539 case BindingScope.CONTEXT:
540 // Use the current context
541 return this;
542 case BindingScope.REQUEST:
543 resolutionCtx = this.getScopedContext(binding.scope);
544 if (resolutionCtx != null) {
545 return resolutionCtx;
546 } else {
547 // If no `REQUEST` scope exists in the chain, fall back to the current
548 // context
549 this.debug(
550 'No context is found for binding "%s (scope=%s)". Fall back to the current context.',
551 binding.key,
552 binding.scope,
553 );
554 return this;
555 }
556 default:
557 // Use the scoped context
558 return this.getScopedContext(binding.scope);
559 }
560 }
561
562 /**
563 * Check if this context is visible (same or ancestor) to the given one
564 * @param ctx - Another context object
565 */
566 isVisibleTo(ctx: Context) {
567 let current: Context | undefined = ctx;
568 while (current != null) {
569 if (current === this) return true;
570 current = current._parent;
571 }
572 return false;
573 }
574
575 /**
576 * Find bindings using a key pattern or filter function
577 * @param pattern - A filter function, a regexp or a wildcard pattern with
578 * optional `*` and `?`. Find returns such bindings where the key matches
579 * the provided pattern.
580 *
581 * For a wildcard:
582 * - `*` matches zero or more characters except `.` and `:`
583 * - `?` matches exactly one character except `.` and `:`
584 *
585 * For a filter function:
586 * - return `true` to include the binding in the results
587 * - return `false` to exclude it.
588 */
589 find<ValueType = BoundValue>(
590 pattern?: string | RegExp | BindingFilter,
591 ): Readonly<Binding<ValueType>>[] {
592 // Optimize if the binding filter is for tags
593 if (typeof pattern === 'function' && isBindingTagFilter(pattern)) {
594 return this._findByTagIndex(pattern.bindingTagPattern);
595 }
596
597 const bindings: Readonly<Binding<ValueType>>[] = [];
598 const filter = filterByKey(pattern);
599
600 for (const b of this.registry.values()) {
601 if (filter(b)) bindings.push(b);
602 }
603
604 const parentBindings = this._parent?.find(filter);
605 return this._mergeWithParent(bindings, parentBindings);
606 }
607
608 /**
609 * Find bindings using the tag filter. If the filter matches one of the
610 * binding tags, the binding is included.
611 *
612 * @param tagFilter - A filter for tags. It can be in one of the following
613 * forms:
614 * - A regular expression, such as `/controller/`
615 * - A wildcard pattern string with optional `*` and `?`, such as `'con*'`
616 * For a wildcard:
617 * - `*` matches zero or more characters except `.` and `:`
618 * - `?` matches exactly one character except `.` and `:`
619 * - An object containing tag name/value pairs, such as
620 * `{name: 'my-controller'}`
621 */
622 findByTag<ValueType = BoundValue>(
623 tagFilter: BindingTag | RegExp,
624 ): Readonly<Binding<ValueType>>[] {
625 return this.find(filterByTag(tagFilter));
626 }
627
628 /**
629 * Find bindings by tag leveraging indexes
630 * @param tag - Tag name pattern or name/value pairs
631 */
632 protected _findByTagIndex<ValueType = BoundValue>(
633 tag: BindingTag | RegExp,
634 ): Readonly<Binding<ValueType>>[] {
635 const currentBindings = this.tagIndexer.findByTagIndex(tag);
636 const parentBindings = this._parent?._findByTagIndex(tag);
637 return this._mergeWithParent(currentBindings, parentBindings);
638 }
639
640 protected _mergeWithParent<ValueType>(
641 childList: Readonly<Binding<ValueType>>[],
642 parentList?: Readonly<Binding<ValueType>>[],
643 ) {
644 if (!parentList) return childList;
645 const additions = parentList.filter(parentBinding => {
646 // children bindings take precedence
647 return !childList.some(
648 childBinding => childBinding.key === parentBinding.key,
649 );
650 });
651 return childList.concat(additions);
652 }
653
654 /**
655 * Get the value bound to the given key, throw an error when no value is
656 * bound for the given key.
657 *
658 * @example
659 *
660 * ```ts
661 * // get the value bound to "application.instance"
662 * const app = await ctx.get<Application>('application.instance');
663 *
664 * // get "rest" property from the value bound to "config"
665 * const config = await ctx.get<RestComponentConfig>('config#rest');
666 *
667 * // get "a" property of "numbers" property from the value bound to "data"
668 * ctx.bind('data').to({numbers: {a: 1, b: 2}, port: 3000});
669 * const a = await ctx.get<number>('data#numbers.a');
670 * ```
671 *
672 * @param keyWithPath - The binding key, optionally suffixed with a path to the
673 * (deeply) nested property to retrieve.
674 * @param session - Optional session for resolution (accepted for backward
675 * compatibility)
676 * @returns A promise of the bound value.
677 */
678 get<ValueType>(
679 keyWithPath: BindingAddress<ValueType>,
680 session?: ResolutionSession,
681 ): Promise<ValueType>;
682
683 /**
684 * Get the value bound to the given key, optionally return a (deep) property
685 * of the bound value.
686 *
687 * @example
688 *
689 * ```ts
690 * // get "rest" property from the value bound to "config"
691 * // use `undefined` when no config is provided
692 * const config = await ctx.get<RestComponentConfig>('config#rest', {
693 * optional: true
694 * });
695 * ```
696 *
697 * @param keyWithPath - The binding key, optionally suffixed with a path to the
698 * (deeply) nested property to retrieve.
699 * @param options - Options for resolution.
700 * @returns A promise of the bound value, or a promise of undefined when
701 * the optional binding is not found.
702 */
703 get<ValueType>(
704 keyWithPath: BindingAddress<ValueType>,
705 options: ResolutionOptions,
706 ): Promise<ValueType | undefined>;
707
708 // Implementation
709 async get<ValueType>(
710 keyWithPath: BindingAddress<ValueType>,
711 optionsOrSession?: ResolutionOptionsOrSession,
712 ): Promise<ValueType | undefined> {
713 this.debug('Resolving binding: %s', keyWithPath);
714 return this.getValueOrPromise<ValueType | undefined>(
715 keyWithPath,
716 optionsOrSession,
717 );
718 }
719
720 /**
721 * Get the synchronous value bound to the given key, optionally
722 * return a (deep) property of the bound value.
723 *
724 * This method throws an error if the bound value requires async computation
725 * (returns a promise). You should never rely on sync bindings in production
726 * code.
727 *
728 * @example
729 *
730 * ```ts
731 * // get the value bound to "application.instance"
732 * const app = ctx.getSync<Application>('application.instance');
733 *
734 * // get "rest" property from the value bound to "config"
735 * const config = await ctx.getSync<RestComponentConfig>('config#rest');
736 * ```
737 *
738 * @param keyWithPath - The binding key, optionally suffixed with a path to the
739 * (deeply) nested property to retrieve.
740 * @param session - Session for resolution (accepted for backward compatibility)
741 * @returns A promise of the bound value.
742 */
743 getSync<ValueType>(
744 keyWithPath: BindingAddress<ValueType>,
745 session?: ResolutionSession,
746 ): ValueType;
747
748 /**
749 * Get the synchronous value bound to the given key, optionally
750 * return a (deep) property of the bound value.
751 *
752 * This method throws an error if the bound value requires async computation
753 * (returns a promise). You should never rely on sync bindings in production
754 * code.
755 *
756 * @example
757 *
758 * ```ts
759 * // get "rest" property from the value bound to "config"
760 * // use "undefined" when no config is provided
761 * const config = await ctx.getSync<RestComponentConfig>('config#rest', {
762 * optional: true
763 * });
764 * ```
765 *
766 * @param keyWithPath - The binding key, optionally suffixed with a path to the
767 * (deeply) nested property to retrieve.
768 * @param options - Options for resolution.
769 * @returns The bound value, or undefined when an optional binding is not found.
770 */
771 getSync<ValueType>(
772 keyWithPath: BindingAddress<ValueType>,
773 options?: ResolutionOptions,
774 ): ValueType | undefined;
775
776 // Implementation
777 getSync<ValueType>(
778 keyWithPath: BindingAddress<ValueType>,
779 optionsOrSession?: ResolutionOptionsOrSession,
780 ): ValueType | undefined {
781 this.debug('Resolving binding synchronously: %s', keyWithPath);
782
783 const valueOrPromise = this.getValueOrPromise<ValueType>(
784 keyWithPath,
785 optionsOrSession,
786 );
787
788 if (isPromiseLike(valueOrPromise)) {
789 throw new Error(
790 `Cannot get ${keyWithPath} synchronously: the value is a promise`,
791 );
792 }
793
794 return valueOrPromise;
795 }
796
797 /**
798 * Look up a binding by key in the context and its ancestors. If no matching
799 * binding is found, an error will be thrown.
800 *
801 * @param key - Binding key
802 */
803 getBinding<ValueType = BoundValue>(
804 key: BindingAddress<ValueType>,
805 ): Binding<ValueType>;
806
807 /**
808 * Look up a binding by key in the context and its ancestors. If no matching
809 * binding is found and `options.optional` is not set to true, an error will
810 * be thrown.
811 *
812 * @param key - Binding key
813 * @param options - Options to control if the binding is optional. If
814 * `options.optional` is set to true, the method will return `undefined`
815 * instead of throwing an error if the binding key is not found.
816 */
817 getBinding<ValueType>(
818 key: BindingAddress<ValueType>,
819 options?: {optional?: boolean},
820 ): Binding<ValueType> | undefined;
821
822 getBinding<ValueType>(
823 key: BindingAddress<ValueType>,
824 options?: {optional?: boolean},
825 ): Binding<ValueType> | undefined {
826 key = BindingKey.validate(key);
827 const binding = this.registry.get(key);
828 if (binding) {
829 return binding;
830 }
831
832 if (this._parent) {
833 return this._parent.getBinding<ValueType>(key, options);
834 }
835
836 if (options?.optional) return undefined;
837 throw new Error(
838 `The key '${key}' is not bound to any value in context ${this.name}`,
839 );
840 }
841
842 /**
843 * Find or create a binding for the given key
844 * @param key - Binding address
845 * @param policy - Binding creation policy
846 */
847 findOrCreateBinding<T>(
848 key: BindingAddress<T>,
849 policy?: BindingCreationPolicy,
850 ) {
851 let binding: Binding<T>;
852 if (policy === BindingCreationPolicy.ALWAYS_CREATE) {
853 binding = this.bind(key);
854 } else if (policy === BindingCreationPolicy.NEVER_CREATE) {
855 binding = this.getBinding(key);
856 } else if (this.isBound(key)) {
857 // CREATE_IF_NOT_BOUND - the key is bound
858 binding = this.getBinding(key);
859 } else {
860 // CREATE_IF_NOT_BOUND - the key is not bound
861 binding = this.bind(key);
862 }
863 return binding;
864 }
865
866 /**
867 * Get the value bound to the given key.
868 *
869 * This is an internal version that preserves the dual sync/async result
870 * of `Binding#getValue()`. Users should use `get()` or `getSync()` instead.
871 *
872 * @example
873 *
874 * ```ts
875 * // get the value bound to "application.instance"
876 * ctx.getValueOrPromise<Application>('application.instance');
877 *
878 * // get "rest" property from the value bound to "config"
879 * ctx.getValueOrPromise<RestComponentConfig>('config#rest');
880 *
881 * // get "a" property of "numbers" property from the value bound to "data"
882 * ctx.bind('data').to({numbers: {a: 1, b: 2}, port: 3000});
883 * ctx.getValueOrPromise<number>('data#numbers.a');
884 * ```
885 *
886 * @param keyWithPath - The binding key, optionally suffixed with a path to the
887 * (deeply) nested property to retrieve.
888 * @param optionsOrSession - Options for resolution or a session
889 * @returns The bound value or a promise of the bound value, depending
890 * on how the binding is configured.
891 * @internal
892 */
893 getValueOrPromise<ValueType>(
894 keyWithPath: BindingAddress<ValueType>,
895 optionsOrSession?: ResolutionOptionsOrSession,
896 ): ValueOrPromise<ValueType | undefined> {
897 const {key, propertyPath} = BindingKey.parseKeyWithPath(keyWithPath);
898
899 const options = asResolutionOptions(optionsOrSession);
900
901 const binding = this.getBinding<ValueType>(key, {optional: true});
902 if (binding == null) {
903 if (options.optional) return undefined;
904 throw new ResolutionError(
905 `The key '${key}' is not bound to any value in context ${this.name}`,
906 {
907 context: this,
908 binding: Binding.bind(key),
909 options,
910 },
911 );
912 }
913
914 const boundValue = binding.getValue(this, options);
915 return propertyPath == null || propertyPath === ''
916 ? boundValue
917 : transformValueOrPromise(boundValue, v =>
918 getDeepProperty<ValueType>(v, propertyPath),
919 );
920 }
921
922 /**
923 * Create a plain JSON object for the context
924 */
925 toJSON(): JSONObject {
926 const bindings: JSONObject = {};
927 for (const [k, v] of this.registry) {
928 bindings[k] = v.toJSON();
929 }
930 return bindings;
931 }
932
933 /**
934 * Inspect the context and dump out a JSON object representing the context
935 * hierarchy
936 * @param options - Options for inspect
937 */
938 // TODO(rfeng): Evaluate https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects
939 inspect(options: ContextInspectOptions = {}): JSONObject {
940 return this._inspect(options, new ClassNameMap());
941 }
942
943 /**
944 * Inspect the context hierarchy
945 * @param options - Options for inspect
946 * @param visitedClasses - A map to keep class to name so that we can have
947 * different names for classes with colliding names. The situation can happen
948 * when two classes with the same name are bound in different modules.
949 */
950 private _inspect(
951 options: ContextInspectOptions,
952 visitedClasses: ClassNameMap,
953 ): JSONObject {
954 options = {
955 includeParent: true,
956 includeInjections: false,
957 ...options,
958 };
959 const bindings: JSONObject = {};
960 for (const [k, v] of this.registry) {
961 const ctor = v.valueConstructor ?? v.providerConstructor;
962 let name: string | undefined = undefined;
963 if (ctor != null) {
964 name = visitedClasses.visit(ctor);
965 }
966 bindings[k] = v.inspect(options);
967 if (name != null) {
968 const binding = bindings[k] as JSONObject;
969 if (v.valueConstructor) {
970 binding.valueConstructor = name;
971 } else if (v.providerConstructor) {
972 binding.providerConstructor = name;
973 }
974 }
975 }
976 const json: JSONObject = {
977 name: this.name,
978 bindings,
979 };
980 if (!options.includeParent) return json;
981 if (this._parent) {
982 json.parent = this._parent._inspect(options, visitedClasses);
983 }
984 return json;
985 }
986
987 /**
988 * The "bind" event is emitted when a new binding is added to the context.
989 * The "unbind" event is emitted when an existing binding is removed.
990 *
991 * @param eventName The name of the event - always `bind` or `unbind`.
992 * @param listener The listener function to call when the event is emitted.
993 */
994 on(eventName: 'bind' | 'unbind', listener: ContextEventListener): this;
995
996 // The generic variant inherited from EventEmitter
997 // eslint-disable-next-line @typescript-eslint/no-explicit-any
998 on(event: string | symbol, listener: (...args: any[]) => void): this;
999
1000 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1001 on(event: string | symbol, listener: (...args: any[]) => void): this {
1002 return super.on(event, listener);
1003 }
1004
1005 /**
1006 * The "bind" event is emitted when a new binding is added to the context.
1007 * The "unbind" event is emitted when an existing binding is removed.
1008 *
1009 * @param eventName The name of the event - always `bind` or `unbind`.
1010 * @param listener The listener function to call when the event is emitted.
1011 */
1012 once(eventName: 'bind' | 'unbind', listener: ContextEventListener): this;
1013
1014 // The generic variant inherited from EventEmitter
1015 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1016 once(event: string | symbol, listener: (...args: any[]) => void): this;
1017
1018 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1019 once(event: string | symbol, listener: (...args: any[]) => void): this {
1020 return super.once(event, listener);
1021 }
1022}
1023
1024/**
1025 * An internal utility class to handle class name conflicts
1026 */
1027class ClassNameMap {
1028 private readonly classes = new Map<Constructor<unknown>, string>();
1029 private readonly nameIndex = new Map<string, number>();
1030
1031 visit(ctor: Constructor<unknown>) {
1032 let name = this.classes.get(ctor);
1033 if (name == null) {
1034 name = ctor.name;
1035 // Now check if the name collides with another class
1036 let index = this.nameIndex.get(name);
1037 if (typeof index === 'number') {
1038 // A conflict is found, mangle the name as `ClassName #1`
1039 this.nameIndex.set(name, ++index);
1040 name = `${name} #${index}`;
1041 } else {
1042 // The name is used for the 1st time
1043 this.nameIndex.set(name, 0);
1044 }
1045 this.classes.set(ctor, name);
1046 }
1047 return name;
1048 }
1049}
1050
1051/**
1052 * Options for context.inspect()
1053 */
1054export interface ContextInspectOptions extends BindingInspectOptions {
1055 /**
1056 * The flag to control if parent context should be inspected
1057 */
1058 includeParent?: boolean;
1059}
1060
1061/**
1062 * Policy to control if a binding should be created for the context
1063 */
1064export enum BindingCreationPolicy {
1065 /**
1066 * Always create a binding with the key for the context
1067 */
1068 ALWAYS_CREATE = 'Always',
1069 /**
1070 * Never create a binding for the context. If the key is not bound in the
1071 * context, throw an error.
1072 */
1073 NEVER_CREATE = 'Never',
1074 /**
1075 * Create a binding if the key is not bound in the context. Otherwise, return
1076 * the existing binding.
1077 */
1078 CREATE_IF_NOT_BOUND = 'IfNotBound',
1079}