1 | ;
|
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
|
6 | Object.defineProperty(exports, "__esModule", { value: true });
|
7 | exports.BindingCreationPolicy = exports.Context = void 0;
|
8 | const tslib_1 = require("tslib");
|
9 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
10 | const events_1 = require("events");
|
11 | const binding_1 = require("./binding");
|
12 | const binding_config_1 = require("./binding-config");
|
13 | const binding_filter_1 = require("./binding-filter");
|
14 | const binding_key_1 = require("./binding-key");
|
15 | const context_subscription_1 = require("./context-subscription");
|
16 | const context_tag_indexer_1 = require("./context-tag-indexer");
|
17 | const context_view_1 = require("./context-view");
|
18 | const keys_1 = require("./keys");
|
19 | const resolution_session_1 = require("./resolution-session");
|
20 | const unique_id_1 = require("./unique-id");
|
21 | const value_promise_1 = require("./value-promise");
|
22 | /**
|
23 | * Context provides an implementation of Inversion of Control (IoC) container
|
24 | */
|
25 | class 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 | }
|
668 | exports.Context = Context;
|
669 | /**
|
670 | * An internal utility class to handle class name conflicts
|
671 | */
|
672 | class 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 | */
|
700 | var 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 |