UNPKG

26.6 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
3// Node module: @loopback/context
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.BindingCreationPolicy = exports.Context = void 0;
8const tslib_1 = require("tslib");
9const debug_1 = tslib_1.__importDefault(require("debug"));
10const events_1 = require("events");
11const binding_1 = require("./binding");
12const binding_config_1 = require("./binding-config");
13const binding_filter_1 = require("./binding-filter");
14const binding_key_1 = require("./binding-key");
15const context_subscription_1 = require("./context-subscription");
16const context_tag_indexer_1 = require("./context-tag-indexer");
17const context_view_1 = require("./context-view");
18const keys_1 = require("./keys");
19const resolution_session_1 = require("./resolution-session");
20const unique_id_1 = require("./unique-id");
21const value_promise_1 = require("./value-promise");
22/**
23 * Context provides an implementation of Inversion of Control (IoC) container
24 */
25class Context extends events_1.EventEmitter {
26 /**
27 * Create a new context.
28 *
29 * @example
30 * ```ts
31 * // Create a new root context, let the framework to create a unique name
32 * const rootCtx = new Context();
33 *
34 * // Create a new child context inheriting bindings from `rootCtx`
35 * const childCtx = new Context(rootCtx);
36 *
37 * // Create another root context called "application"
38 * const appCtx = new Context('application');
39 *
40 * // Create a new child context called "request" and inheriting bindings
41 * // from `appCtx`
42 * const reqCtx = new Context(appCtx, 'request');
43 * ```
44 * @param _parent - The optional parent context
45 * @param name - Name of the context. If not provided, a unique identifier
46 * will be generated as the name.
47 */
48 constructor(_parent, name) {
49 super();
50 /**
51 * Key to binding map as the internal registry
52 */
53 this.registry = new Map();
54 /**
55 * Scope for binding resolution
56 */
57 this.scope = binding_1.BindingScope.CONTEXT;
58 // The number of listeners can grow with the number of child contexts
59 // For example, each request can add a listener to the RestServer and the
60 // listener is removed when the request processing is finished.
61 // See https://github.com/loopbackio/loopback-next/issues/4363
62 this.setMaxListeners(Infinity);
63 if (typeof _parent === 'string') {
64 name = _parent;
65 _parent = undefined;
66 }
67 this._parent = _parent;
68 this.name = name !== null && name !== void 0 ? name : this.generateName();
69 this.tagIndexer = new context_tag_indexer_1.ContextTagIndexer(this);
70 this.subscriptionManager = new context_subscription_1.ContextSubscriptionManager(this);
71 this._debug = (0, debug_1.default)(this.getDebugNamespace());
72 }
73 /**
74 * Get the debug namespace for the context class. Subclasses can override
75 * this method to supply its own namespace.
76 *
77 * @example
78 * ```ts
79 * export class Application extends Context {
80 * super('application');
81 * }
82 *
83 * protected getDebugNamespace() {
84 * return 'loopback:context:application';
85 * }
86 * ```
87 */
88 getDebugNamespace() {
89 if (this.constructor === Context)
90 return 'loopback:context';
91 const name = this.constructor.name.toLowerCase();
92 return `loopback:context:${name}`;
93 }
94 generateName() {
95 const id = (0, unique_id_1.generateUniqueId)();
96 if (this.constructor === Context)
97 return id;
98 return `${this.constructor.name}-${id}`;
99 }
100 /**
101 * @internal
102 * Getter for ContextSubscriptionManager
103 */
104 get parent() {
105 return this._parent;
106 }
107 /**
108 * Wrap the debug statement so that it always print out the context name
109 * as the prefix
110 * @param args - Arguments for the debug
111 */
112 debug(...args) {
113 /* istanbul ignore if */
114 if (!this._debug.enabled)
115 return;
116 const formatter = args.shift();
117 if (typeof formatter === 'string') {
118 this._debug(`[%s] ${formatter}`, this.name, ...args);
119 }
120 else {
121 this._debug('[%s] ', this.name, formatter, ...args);
122 }
123 }
124 /**
125 * A strongly-typed method to emit context events
126 * @param type Event type
127 * @param event Context event
128 */
129 emitEvent(type, event) {
130 this.emit(type, event);
131 }
132 /**
133 * Emit an `error` event
134 * @param err Error
135 */
136 emitError(err) {
137 this.emit('error', err);
138 }
139 /**
140 * Create a binding with the given key in the context. If a locked binding
141 * already exists with the same key, an error will be thrown.
142 *
143 * @param key - Binding key
144 */
145 bind(key) {
146 const binding = new binding_1.Binding(key.toString());
147 this.add(binding);
148 return binding;
149 }
150 /**
151 * Add a binding to the context. If a locked binding already exists with the
152 * same key, an error will be thrown.
153 * @param binding - The configured binding to be added
154 */
155 add(binding) {
156 const key = binding.key;
157 this.debug('[%s] Adding binding: %s', key);
158 let existingBinding;
159 const keyExists = this.registry.has(key);
160 if (keyExists) {
161 existingBinding = this.registry.get(key);
162 const bindingIsLocked = existingBinding === null || existingBinding === void 0 ? void 0 : existingBinding.isLocked;
163 if (bindingIsLocked)
164 throw new Error(`Cannot rebind key "${key}" to a locked binding`);
165 }
166 this.registry.set(key, binding);
167 if (existingBinding !== binding) {
168 if (existingBinding != null) {
169 this.emitEvent('unbind', {
170 binding: existingBinding,
171 context: this,
172 type: 'unbind',
173 });
174 }
175 this.emitEvent('bind', { binding, context: this, type: 'bind' });
176 }
177 return this;
178 }
179 /**
180 * Create a corresponding binding for configuration of the target bound by
181 * the given key in the context.
182 *
183 * For example, `ctx.configure('controllers.MyController').to({x: 1})` will
184 * create binding `controllers.MyController:$config` with value `{x: 1}`.
185 *
186 * @param key - The key for the binding to be configured
187 */
188 configure(key = '') {
189 const bindingForConfig = binding_1.Binding.configure(key);
190 this.add(bindingForConfig);
191 return bindingForConfig;
192 }
193 /**
194 * Get the value or promise of configuration for a given binding by key
195 *
196 * @param key - Binding key
197 * @param propertyPath - Property path for the option. For example, `x.y`
198 * requests for `<config>.x.y`. If not set, the `<config>` object will be
199 * returned.
200 * @param resolutionOptions - Options for the resolution.
201 * - optional: if not set or set to `true`, `undefined` will be returned if
202 * no corresponding value is found. Otherwise, an error will be thrown.
203 */
204 getConfigAsValueOrPromise(key, propertyPath, resolutionOptions) {
205 this.setupConfigurationResolverIfNeeded();
206 return this.configResolver.getConfigAsValueOrPromise(key, propertyPath, resolutionOptions);
207 }
208 /**
209 * Set up the configuration resolver if needed
210 */
211 setupConfigurationResolverIfNeeded() {
212 if (!this.configResolver) {
213 // First try the bound ConfigurationResolver to this context
214 const configResolver = this.getSync(keys_1.ContextBindings.CONFIGURATION_RESOLVER, {
215 optional: true,
216 });
217 if (configResolver) {
218 this.debug('Custom ConfigurationResolver is loaded from %s.', keys_1.ContextBindings.CONFIGURATION_RESOLVER.toString());
219 this.configResolver = configResolver;
220 }
221 else {
222 // Fallback to DefaultConfigurationResolver
223 this.debug('DefaultConfigurationResolver is used.');
224 this.configResolver = new binding_config_1.DefaultConfigurationResolver(this);
225 }
226 }
227 return this.configResolver;
228 }
229 /**
230 * Resolve configuration for the binding by key
231 *
232 * @param key - Binding key
233 * @param propertyPath - Property path for the option. For example, `x.y`
234 * requests for `<config>.x.y`. If not set, the `<config>` object will be
235 * returned.
236 * @param resolutionOptions - Options for the resolution.
237 */
238 async getConfig(key, propertyPath, resolutionOptions) {
239 return this.getConfigAsValueOrPromise(key, propertyPath, resolutionOptions);
240 }
241 /**
242 * Resolve configuration synchronously for the binding by key
243 *
244 * @param key - Binding key
245 * @param propertyPath - Property path for the option. For example, `x.y`
246 * requests for `config.x.y`. If not set, the `config` object will be
247 * returned.
248 * @param resolutionOptions - Options for the resolution.
249 */
250 getConfigSync(key, propertyPath, resolutionOptions) {
251 const valueOrPromise = this.getConfigAsValueOrPromise(key, propertyPath, resolutionOptions);
252 if ((0, value_promise_1.isPromiseLike)(valueOrPromise)) {
253 const prop = propertyPath ? ` property ${propertyPath}` : '';
254 throw new Error(`Cannot get config${prop} for ${key} synchronously: the value is a promise`);
255 }
256 return valueOrPromise;
257 }
258 /**
259 * Unbind a binding from the context. No parent contexts will be checked.
260 *
261 * @remarks
262 * If you need to unbind a binding owned by a parent context, use the code
263 * below:
264 *
265 * ```ts
266 * const ownerCtx = ctx.getOwnerContext(key);
267 * return ownerCtx != null && ownerCtx.unbind(key);
268 * ```
269 *
270 * @param key - Binding key
271 * @returns true if the binding key is found and removed from this context
272 */
273 unbind(key) {
274 this.debug('Unbind %s', key);
275 key = binding_key_1.BindingKey.validate(key);
276 const binding = this.registry.get(key);
277 // If not found, return `false`
278 if (binding == null)
279 return false;
280 if (binding === null || binding === void 0 ? void 0 : binding.isLocked)
281 throw new Error(`Cannot unbind key "${key}" of a locked binding`);
282 this.registry.delete(key);
283 this.emitEvent('unbind', { binding, context: this, type: 'unbind' });
284 return true;
285 }
286 /**
287 * Add a context event observer to the context
288 * @param observer - Context observer instance or function
289 */
290 subscribe(observer) {
291 return this.subscriptionManager.subscribe(observer);
292 }
293 /**
294 * Remove the context event observer from the context
295 * @param observer - Context event observer
296 */
297 unsubscribe(observer) {
298 return this.subscriptionManager.unsubscribe(observer);
299 }
300 /**
301 * Close the context: clear observers, stop notifications, and remove event
302 * listeners from its parent context.
303 *
304 * @remarks
305 * This method MUST be called to avoid memory leaks once a context object is
306 * no longer needed and should be recycled. An example is the `RequestContext`,
307 * which is created per request.
308 */
309 close() {
310 this.debug('Closing context...');
311 this.subscriptionManager.close();
312 this.tagIndexer.close();
313 }
314 /**
315 * Check if an observer is subscribed to this context
316 * @param observer - Context observer
317 */
318 isSubscribed(observer) {
319 return this.subscriptionManager.isSubscribed(observer);
320 }
321 /**
322 * Create a view of the context chain with the given binding filter
323 * @param filter - A function to match bindings
324 * @param comparator - A function to sort matched bindings
325 * @param options - Resolution options
326 */
327 createView(filter, comparator, options) {
328 const view = new context_view_1.ContextView(this, filter, comparator, options);
329 view.open();
330 return view;
331 }
332 /**
333 * Check if a binding exists with the given key in the local context without
334 * delegating to the parent context
335 * @param key - Binding key
336 */
337 contains(key) {
338 key = binding_key_1.BindingKey.validate(key);
339 return this.registry.has(key);
340 }
341 /**
342 * Check if a key is bound in the context or its ancestors
343 * @param key - Binding key
344 */
345 isBound(key) {
346 if (this.contains(key))
347 return true;
348 if (this._parent) {
349 return this._parent.isBound(key);
350 }
351 return false;
352 }
353 /**
354 * Get the owning context for a binding or its key
355 * @param keyOrBinding - Binding object or key
356 */
357 getOwnerContext(keyOrBinding) {
358 let key;
359 if (keyOrBinding instanceof binding_1.Binding) {
360 key = keyOrBinding.key;
361 }
362 else {
363 key = keyOrBinding;
364 }
365 if (this.contains(key)) {
366 if (keyOrBinding instanceof binding_1.Binding) {
367 // Check if the contained binding is the same
368 if (this.registry.get(key.toString()) === keyOrBinding) {
369 return this;
370 }
371 return undefined;
372 }
373 return this;
374 }
375 if (this._parent) {
376 return this._parent.getOwnerContext(key);
377 }
378 return undefined;
379 }
380 /**
381 * Get the context matching the scope
382 * @param scope - Binding scope
383 */
384 getScopedContext(scope) {
385 if (this.scope === scope)
386 return this;
387 if (this._parent) {
388 return this._parent.getScopedContext(scope);
389 }
390 return undefined;
391 }
392 /**
393 * Locate the resolution context for the given binding. Only bindings in the
394 * resolution context and its ancestors are visible as dependencies to resolve
395 * the given binding
396 * @param binding - Binding object
397 */
398 getResolutionContext(binding) {
399 let resolutionCtx;
400 switch (binding.scope) {
401 case binding_1.BindingScope.SINGLETON:
402 // Use the owner context
403 return this.getOwnerContext(binding.key);
404 case binding_1.BindingScope.TRANSIENT:
405 case binding_1.BindingScope.CONTEXT:
406 // Use the current context
407 return this;
408 case binding_1.BindingScope.REQUEST:
409 resolutionCtx = this.getScopedContext(binding.scope);
410 if (resolutionCtx != null) {
411 return resolutionCtx;
412 }
413 else {
414 // If no `REQUEST` scope exists in the chain, fall back to the current
415 // context
416 this.debug('No context is found for binding "%s (scope=%s)". Fall back to the current context.', binding.key, binding.scope);
417 return this;
418 }
419 default:
420 // Use the scoped context
421 return this.getScopedContext(binding.scope);
422 }
423 }
424 /**
425 * Check if this context is visible (same or ancestor) to the given one
426 * @param ctx - Another context object
427 */
428 isVisibleTo(ctx) {
429 let current = ctx;
430 while (current != null) {
431 if (current === this)
432 return true;
433 current = current._parent;
434 }
435 return false;
436 }
437 /**
438 * Find bindings using a key pattern or filter function
439 * @param pattern - A filter function, a regexp or a wildcard pattern with
440 * optional `*` and `?`. Find returns such bindings where the key matches
441 * the provided pattern.
442 *
443 * For a wildcard:
444 * - `*` matches zero or more characters except `.` and `:`
445 * - `?` matches exactly one character except `.` and `:`
446 *
447 * For a filter function:
448 * - return `true` to include the binding in the results
449 * - return `false` to exclude it.
450 */
451 find(pattern) {
452 var _a;
453 // Optimize if the binding filter is for tags
454 if (typeof pattern === 'function' && (0, binding_filter_1.isBindingTagFilter)(pattern)) {
455 return this._findByTagIndex(pattern.bindingTagPattern);
456 }
457 const bindings = [];
458 const filter = (0, binding_filter_1.filterByKey)(pattern);
459 for (const b of this.registry.values()) {
460 if (filter(b))
461 bindings.push(b);
462 }
463 const parentBindings = (_a = this._parent) === null || _a === void 0 ? void 0 : _a.find(filter);
464 return this._mergeWithParent(bindings, parentBindings);
465 }
466 /**
467 * Find bindings using the tag filter. If the filter matches one of the
468 * binding tags, the binding is included.
469 *
470 * @param tagFilter - A filter for tags. It can be in one of the following
471 * forms:
472 * - A regular expression, such as `/controller/`
473 * - A wildcard pattern string with optional `*` and `?`, such as `'con*'`
474 * For a wildcard:
475 * - `*` matches zero or more characters except `.` and `:`
476 * - `?` matches exactly one character except `.` and `:`
477 * - An object containing tag name/value pairs, such as
478 * `{name: 'my-controller'}`
479 */
480 findByTag(tagFilter) {
481 return this.find((0, binding_filter_1.filterByTag)(tagFilter));
482 }
483 /**
484 * Find bindings by tag leveraging indexes
485 * @param tag - Tag name pattern or name/value pairs
486 */
487 _findByTagIndex(tag) {
488 var _a;
489 const currentBindings = this.tagIndexer.findByTagIndex(tag);
490 const parentBindings = (_a = this._parent) === null || _a === void 0 ? void 0 : _a._findByTagIndex(tag);
491 return this._mergeWithParent(currentBindings, parentBindings);
492 }
493 _mergeWithParent(childList, parentList) {
494 if (!parentList)
495 return childList;
496 const additions = parentList.filter(parentBinding => {
497 // children bindings take precedence
498 return !childList.some(childBinding => childBinding.key === parentBinding.key);
499 });
500 return childList.concat(additions);
501 }
502 // Implementation
503 async get(keyWithPath, optionsOrSession) {
504 this.debug('Resolving binding: %s', keyWithPath);
505 return this.getValueOrPromise(keyWithPath, optionsOrSession);
506 }
507 // Implementation
508 getSync(keyWithPath, optionsOrSession) {
509 this.debug('Resolving binding synchronously: %s', keyWithPath);
510 const valueOrPromise = this.getValueOrPromise(keyWithPath, optionsOrSession);
511 if ((0, value_promise_1.isPromiseLike)(valueOrPromise)) {
512 throw new Error(`Cannot get ${keyWithPath} synchronously: the value is a promise`);
513 }
514 return valueOrPromise;
515 }
516 getBinding(key, options) {
517 key = binding_key_1.BindingKey.validate(key);
518 const binding = this.registry.get(key);
519 if (binding) {
520 return binding;
521 }
522 if (this._parent) {
523 return this._parent.getBinding(key, options);
524 }
525 if (options === null || options === void 0 ? void 0 : options.optional)
526 return undefined;
527 throw new Error(`The key '${key}' is not bound to any value in context ${this.name}`);
528 }
529 /**
530 * Find or create a binding for the given key
531 * @param key - Binding address
532 * @param policy - Binding creation policy
533 */
534 findOrCreateBinding(key, policy) {
535 let binding;
536 if (policy === BindingCreationPolicy.ALWAYS_CREATE) {
537 binding = this.bind(key);
538 }
539 else if (policy === BindingCreationPolicy.NEVER_CREATE) {
540 binding = this.getBinding(key);
541 }
542 else if (this.isBound(key)) {
543 // CREATE_IF_NOT_BOUND - the key is bound
544 binding = this.getBinding(key);
545 }
546 else {
547 // CREATE_IF_NOT_BOUND - the key is not bound
548 binding = this.bind(key);
549 }
550 return binding;
551 }
552 /**
553 * Get the value bound to the given key.
554 *
555 * This is an internal version that preserves the dual sync/async result
556 * of `Binding#getValue()`. Users should use `get()` or `getSync()` instead.
557 *
558 * @example
559 *
560 * ```ts
561 * // get the value bound to "application.instance"
562 * ctx.getValueOrPromise<Application>('application.instance');
563 *
564 * // get "rest" property from the value bound to "config"
565 * ctx.getValueOrPromise<RestComponentConfig>('config#rest');
566 *
567 * // get "a" property of "numbers" property from the value bound to "data"
568 * ctx.bind('data').to({numbers: {a: 1, b: 2}, port: 3000});
569 * ctx.getValueOrPromise<number>('data#numbers.a');
570 * ```
571 *
572 * @param keyWithPath - The binding key, optionally suffixed with a path to the
573 * (deeply) nested property to retrieve.
574 * @param optionsOrSession - Options for resolution or a session
575 * @returns The bound value or a promise of the bound value, depending
576 * on how the binding is configured.
577 * @internal
578 */
579 getValueOrPromise(keyWithPath, optionsOrSession) {
580 const { key, propertyPath } = binding_key_1.BindingKey.parseKeyWithPath(keyWithPath);
581 const options = (0, resolution_session_1.asResolutionOptions)(optionsOrSession);
582 const binding = this.getBinding(key, { optional: true });
583 if (binding == null) {
584 if (options.optional)
585 return undefined;
586 throw new resolution_session_1.ResolutionError(`The key '${key}' is not bound to any value in context ${this.name}`, {
587 context: this,
588 binding: binding_1.Binding.bind(key),
589 options,
590 });
591 }
592 const boundValue = binding.getValue(this, options);
593 return propertyPath == null || propertyPath === ''
594 ? boundValue
595 : (0, value_promise_1.transformValueOrPromise)(boundValue, v => (0, value_promise_1.getDeepProperty)(v, propertyPath));
596 }
597 /**
598 * Create a plain JSON object for the context
599 */
600 toJSON() {
601 const bindings = {};
602 for (const [k, v] of this.registry) {
603 bindings[k] = v.toJSON();
604 }
605 return bindings;
606 }
607 /**
608 * Inspect the context and dump out a JSON object representing the context
609 * hierarchy
610 * @param options - Options for inspect
611 */
612 // TODO(rfeng): Evaluate https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects
613 inspect(options = {}) {
614 return this._inspect(options, new ClassNameMap());
615 }
616 /**
617 * Inspect the context hierarchy
618 * @param options - Options for inspect
619 * @param visitedClasses - A map to keep class to name so that we can have
620 * different names for classes with colliding names. The situation can happen
621 * when two classes with the same name are bound in different modules.
622 */
623 _inspect(options, visitedClasses) {
624 var _a;
625 options = {
626 includeParent: true,
627 includeInjections: false,
628 ...options,
629 };
630 const bindings = {};
631 for (const [k, v] of this.registry) {
632 const ctor = (_a = v.valueConstructor) !== null && _a !== void 0 ? _a : v.providerConstructor;
633 let name = undefined;
634 if (ctor != null) {
635 name = visitedClasses.visit(ctor);
636 }
637 bindings[k] = v.inspect(options);
638 if (name != null) {
639 const binding = bindings[k];
640 if (v.valueConstructor) {
641 binding.valueConstructor = name;
642 }
643 else if (v.providerConstructor) {
644 binding.providerConstructor = name;
645 }
646 }
647 }
648 const json = {
649 name: this.name,
650 bindings,
651 };
652 if (!options.includeParent)
653 return json;
654 if (this._parent) {
655 json.parent = this._parent._inspect(options, visitedClasses);
656 }
657 return json;
658 }
659 // eslint-disable-next-line @typescript-eslint/no-explicit-any
660 on(event, listener) {
661 return super.on(event, listener);
662 }
663 // eslint-disable-next-line @typescript-eslint/no-explicit-any
664 once(event, listener) {
665 return super.once(event, listener);
666 }
667}
668exports.Context = Context;
669/**
670 * An internal utility class to handle class name conflicts
671 */
672class ClassNameMap {
673 constructor() {
674 this.classes = new Map();
675 this.nameIndex = new Map();
676 }
677 visit(ctor) {
678 let name = this.classes.get(ctor);
679 if (name == null) {
680 name = ctor.name;
681 // Now check if the name collides with another class
682 let index = this.nameIndex.get(name);
683 if (typeof index === 'number') {
684 // A conflict is found, mangle the name as `ClassName #1`
685 this.nameIndex.set(name, ++index);
686 name = `${name} #${index}`;
687 }
688 else {
689 // The name is used for the 1st time
690 this.nameIndex.set(name, 0);
691 }
692 this.classes.set(ctor, name);
693 }
694 return name;
695 }
696}
697/**
698 * Policy to control if a binding should be created for the context
699 */
700var BindingCreationPolicy;
701(function (BindingCreationPolicy) {
702 /**
703 * Always create a binding with the key for the context
704 */
705 BindingCreationPolicy["ALWAYS_CREATE"] = "Always";
706 /**
707 * Never create a binding for the context. If the key is not bound in the
708 * context, throw an error.
709 */
710 BindingCreationPolicy["NEVER_CREATE"] = "Never";
711 /**
712 * Create a binding if the key is not bound in the context. Otherwise, return
713 * the existing binding.
714 */
715 BindingCreationPolicy["CREATE_IF_NOT_BOUND"] = "IfNotBound";
716})(BindingCreationPolicy || (exports.BindingCreationPolicy = BindingCreationPolicy = {}));
717//# sourceMappingURL=context.js.map
\No newline at end of file