@loopback/context 4.0.0-alpha.7 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +116 -0
  3. package/dist/binding-config.d.ts +40 -0
  4. package/dist/binding-config.js +33 -0
  5. package/dist/binding-config.js.map +1 -0
  6. package/dist/binding-decorator.d.ts +45 -0
  7. package/dist/binding-decorator.js +118 -0
  8. package/dist/binding-decorator.js.map +1 -0
  9. package/dist/binding-filter.d.ts +108 -0
  10. package/dist/binding-filter.js +162 -0
  11. package/dist/binding-filter.js.map +1 -0
  12. package/dist/binding-inspector.d.ts +150 -0
  13. package/dist/binding-inspector.js +249 -0
  14. package/dist/binding-inspector.js.map +1 -0
  15. package/dist/binding-key.d.ts +66 -0
  16. package/dist/binding-key.js +121 -0
  17. package/dist/binding-key.js.map +1 -0
  18. package/dist/binding-sorter.d.ts +71 -0
  19. package/dist/binding-sorter.js +89 -0
  20. package/dist/binding-sorter.js.map +1 -0
  21. package/dist/binding.d.ts +577 -0
  22. package/dist/binding.js +788 -0
  23. package/dist/binding.js.map +1 -0
  24. package/dist/context-event.d.ts +23 -0
  25. package/dist/context-event.js +7 -0
  26. package/dist/context-event.js.map +1 -0
  27. package/dist/context-observer.d.ts +36 -0
  28. package/dist/context-observer.js +7 -0
  29. package/dist/context-observer.js.map +1 -0
  30. package/dist/context-subscription.d.ts +147 -0
  31. package/dist/context-subscription.js +317 -0
  32. package/dist/context-subscription.js.map +1 -0
  33. package/dist/context-tag-indexer.d.ts +42 -0
  34. package/dist/context-tag-indexer.js +135 -0
  35. package/dist/context-tag-indexer.js.map +1 -0
  36. package/dist/context-view.d.ts +209 -0
  37. package/dist/context-view.js +240 -0
  38. package/dist/context-view.js.map +1 -0
  39. package/dist/context.d.ts +513 -0
  40. package/dist/context.js +717 -0
  41. package/dist/context.js.map +1 -0
  42. package/dist/index.d.ts +52 -0
  43. package/dist/index.js +60 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/inject-config.d.ts +67 -0
  46. package/dist/inject-config.js +181 -0
  47. package/dist/inject-config.js.map +1 -0
  48. package/dist/inject.d.ts +250 -0
  49. package/dist/inject.js +535 -0
  50. package/dist/inject.js.map +1 -0
  51. package/dist/interception-proxy.d.ts +76 -0
  52. package/dist/interception-proxy.js +67 -0
  53. package/dist/interception-proxy.js.map +1 -0
  54. package/dist/interceptor-chain.d.ts +121 -0
  55. package/dist/interceptor-chain.js +148 -0
  56. package/dist/interceptor-chain.js.map +1 -0
  57. package/dist/interceptor.d.ts +138 -0
  58. package/dist/interceptor.js +299 -0
  59. package/dist/interceptor.js.map +1 -0
  60. package/dist/invocation.d.ts +101 -0
  61. package/dist/invocation.js +163 -0
  62. package/dist/invocation.js.map +1 -0
  63. package/dist/json-types.d.ts +28 -0
  64. package/dist/json-types.js +7 -0
  65. package/dist/json-types.js.map +1 -0
  66. package/dist/keys.d.ts +65 -0
  67. package/dist/keys.js +74 -0
  68. package/dist/keys.js.map +1 -0
  69. package/dist/provider.d.ts +31 -0
  70. package/dist/provider.js +7 -0
  71. package/dist/provider.js.map +1 -0
  72. package/dist/resolution-session.d.ts +180 -0
  73. package/dist/resolution-session.js +274 -0
  74. package/dist/resolution-session.js.map +1 -0
  75. package/dist/resolver.d.ts +46 -0
  76. package/dist/resolver.js +203 -0
  77. package/dist/resolver.js.map +1 -0
  78. package/dist/unique-id.d.ts +14 -0
  79. package/dist/unique-id.js +26 -0
  80. package/dist/unique-id.js.map +1 -0
  81. package/dist/value-promise.d.ts +134 -0
  82. package/dist/value-promise.js +277 -0
  83. package/dist/value-promise.js.map +1 -0
  84. package/package.json +49 -35
  85. package/src/binding-config.ts +73 -0
  86. package/src/binding-decorator.ts +136 -0
  87. package/src/binding-filter.ts +250 -0
  88. package/src/binding-inspector.ts +371 -0
  89. package/src/binding-key.ts +136 -0
  90. package/src/binding-sorter.ts +124 -0
  91. package/src/binding.ts +1107 -0
  92. package/src/context-event.ts +30 -0
  93. package/src/context-observer.ts +50 -0
  94. package/src/context-subscription.ts +402 -0
  95. package/src/context-tag-indexer.ts +147 -0
  96. package/src/context-view.ts +440 -0
  97. package/src/context.ts +1079 -0
  98. package/src/index.ts +58 -0
  99. package/src/inject-config.ts +239 -0
  100. package/src/inject.ts +796 -0
  101. package/src/interception-proxy.ts +127 -0
  102. package/src/interceptor-chain.ts +268 -0
  103. package/src/interceptor.ts +430 -0
  104. package/src/invocation.ts +269 -0
  105. package/src/json-types.ts +35 -0
  106. package/src/keys.ts +85 -0
  107. package/src/provider.ts +37 -0
  108. package/src/resolution-session.ts +414 -0
  109. package/src/resolver.ts +282 -0
  110. package/src/unique-id.ts +24 -0
  111. package/src/value-promise.ts +318 -0
  112. package/index.d.ts +0 -6
  113. package/index.js +0 -9
  114. package/lib/binding.d.ts +0 -75
  115. package/lib/binding.js +0 -102
  116. package/lib/context.d.ts +0 -14
  117. package/lib/context.js +0 -96
  118. package/lib/index.d.ts +0 -5
  119. package/lib/index.js +0 -13
  120. package/lib/inject.d.ts +0 -47
  121. package/lib/inject.js +0 -73
  122. package/lib/isPromise.d.ts +0 -1
  123. package/lib/isPromise.js +0 -14
  124. package/lib/resolver.d.ts +0 -30
  125. package/lib/resolver.js +0 -128
  126. package/lib6/binding.d.ts +0 -75
  127. package/lib6/binding.js +0 -102
  128. package/lib6/context.d.ts +0 -14
  129. package/lib6/context.js +0 -96
  130. package/lib6/index.d.ts +0 -5
  131. package/lib6/index.js +0 -13
  132. package/lib6/inject.d.ts +0 -47
  133. package/lib6/inject.js +0 -73
  134. package/lib6/isPromise.d.ts +0 -1
  135. package/lib6/isPromise.js +0 -14
  136. package/lib6/resolver.d.ts +0 -30
  137. package/lib6/resolver.js +0 -128
package/src/context.ts ADDED
@@ -0,0 +1,1079 @@
1
+ // Copyright IBM Corp. 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
+
6
+ import debugFactory, {Debugger} from 'debug';
7
+ import {EventEmitter} from 'events';
8
+ import {
9
+ Binding,
10
+ BindingInspectOptions,
11
+ BindingScope,
12
+ BindingTag,
13
+ } from './binding';
14
+ import {
15
+ ConfigurationResolver,
16
+ DefaultConfigurationResolver,
17
+ } from './binding-config';
18
+ import {
19
+ BindingFilter,
20
+ filterByKey,
21
+ filterByTag,
22
+ isBindingTagFilter,
23
+ } from './binding-filter';
24
+ import {BindingAddress, BindingKey} from './binding-key';
25
+ import {BindingComparator} from './binding-sorter';
26
+ import {ContextEvent, ContextEventListener} from './context-event';
27
+ import {ContextEventObserver, ContextObserver} from './context-observer';
28
+ import {ContextSubscriptionManager, Subscription} from './context-subscription';
29
+ import {ContextTagIndexer} from './context-tag-indexer';
30
+ import {ContextView} from './context-view';
31
+ import {JSONObject} from './json-types';
32
+ import {ContextBindings} from './keys';
33
+ import {
34
+ asResolutionOptions,
35
+ ResolutionError,
36
+ ResolutionOptions,
37
+ ResolutionOptionsOrSession,
38
+ ResolutionSession,
39
+ } from './resolution-session';
40
+ import {generateUniqueId} from './unique-id';
41
+ import {
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
+ */
53
+ export 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
+ */
1027
+ class 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
+ */
1054
+ export 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
+ */
1064
+ export 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
+ }