@loopback/context 4.0.0-alpha.8 → 4.1.0

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 (201) 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/{lib/src/provider.js → dist/json-types.js} +3 -3
  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/{lib6/src → dist}/provider.js +2 -2
  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 -34
  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 -103
  116. package/lib/binding.js.map +0 -1
  117. package/lib/context.d.ts +0 -14
  118. package/lib/context.js +0 -97
  119. package/lib/context.js.map +0 -1
  120. package/lib/index.d.ts +0 -1
  121. package/lib/index.js +0 -12
  122. package/lib/index.js.map +0 -1
  123. package/lib/inject.d.ts +0 -46
  124. package/lib/inject.js +0 -74
  125. package/lib/inject.js.map +0 -1
  126. package/lib/isPromise.d.ts +0 -1
  127. package/lib/isPromise.js +0 -15
  128. package/lib/isPromise.js.map +0 -1
  129. package/lib/reflect.d.ts +0 -39
  130. package/lib/reflect.js +0 -20
  131. package/lib/reflect.js.map +0 -1
  132. package/lib/resolver.d.ts +0 -30
  133. package/lib/resolver.js +0 -129
  134. package/lib/resolver.js.map +0 -1
  135. package/lib/src/binding.d.ts +0 -85
  136. package/lib/src/binding.js +0 -123
  137. package/lib/src/binding.js.map +0 -1
  138. package/lib/src/context.d.ts +0 -14
  139. package/lib/src/context.js +0 -97
  140. package/lib/src/context.js.map +0 -1
  141. package/lib/src/index.d.ts +0 -10
  142. package/lib/src/index.js +0 -27
  143. package/lib/src/index.js.map +0 -1
  144. package/lib/src/inject.d.ts +0 -46
  145. package/lib/src/inject.js +0 -74
  146. package/lib/src/inject.js.map +0 -1
  147. package/lib/src/isPromise.d.ts +0 -1
  148. package/lib/src/isPromise.js +0 -15
  149. package/lib/src/isPromise.js.map +0 -1
  150. package/lib/src/provider.d.ts +0 -29
  151. package/lib/src/provider.js.map +0 -1
  152. package/lib/src/reflect.d.ts +0 -38
  153. package/lib/src/reflect.js +0 -143
  154. package/lib/src/reflect.js.map +0 -1
  155. package/lib/src/resolver.d.ts +0 -34
  156. package/lib/src/resolver.js +0 -144
  157. package/lib/src/resolver.js.map +0 -1
  158. package/lib6/binding.d.ts +0 -75
  159. package/lib6/binding.js +0 -103
  160. package/lib6/binding.js.map +0 -1
  161. package/lib6/context.d.ts +0 -14
  162. package/lib6/context.js +0 -97
  163. package/lib6/context.js.map +0 -1
  164. package/lib6/index.d.ts +0 -1
  165. package/lib6/index.js +0 -12
  166. package/lib6/index.js.map +0 -1
  167. package/lib6/inject.d.ts +0 -46
  168. package/lib6/inject.js +0 -74
  169. package/lib6/inject.js.map +0 -1
  170. package/lib6/isPromise.d.ts +0 -1
  171. package/lib6/isPromise.js +0 -15
  172. package/lib6/isPromise.js.map +0 -1
  173. package/lib6/reflect.d.ts +0 -39
  174. package/lib6/reflect.js +0 -20
  175. package/lib6/reflect.js.map +0 -1
  176. package/lib6/resolver.d.ts +0 -30
  177. package/lib6/resolver.js +0 -129
  178. package/lib6/resolver.js.map +0 -1
  179. package/lib6/src/binding.d.ts +0 -85
  180. package/lib6/src/binding.js +0 -133
  181. package/lib6/src/binding.js.map +0 -1
  182. package/lib6/src/context.d.ts +0 -14
  183. package/lib6/src/context.js +0 -97
  184. package/lib6/src/context.js.map +0 -1
  185. package/lib6/src/index.d.ts +0 -10
  186. package/lib6/src/index.js +0 -27
  187. package/lib6/src/index.js.map +0 -1
  188. package/lib6/src/inject.d.ts +0 -46
  189. package/lib6/src/inject.js +0 -74
  190. package/lib6/src/inject.js.map +0 -1
  191. package/lib6/src/isPromise.d.ts +0 -1
  192. package/lib6/src/isPromise.js +0 -15
  193. package/lib6/src/isPromise.js.map +0 -1
  194. package/lib6/src/provider.d.ts +0 -29
  195. package/lib6/src/provider.js.map +0 -1
  196. package/lib6/src/reflect.d.ts +0 -38
  197. package/lib6/src/reflect.js +0 -143
  198. package/lib6/src/reflect.js.map +0 -1
  199. package/lib6/src/resolver.d.ts +0 -34
  200. package/lib6/src/resolver.js +0 -154
  201. package/lib6/src/resolver.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ // Copyright IBM Corp. 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 {Binding} from './binding';
7
+ import {Context} from './context';
8
+
9
+ /**
10
+ * Events emitted by a context
11
+ */
12
+ export type ContextEvent = {
13
+ /**
14
+ * Source context that emits the event
15
+ */
16
+ context: Context;
17
+ /**
18
+ * Binding that is being added/removed/updated
19
+ */
20
+ binding: Readonly<Binding<unknown>>;
21
+ /**
22
+ * Event type
23
+ */
24
+ type: string; // 'bind' or 'unbind'
25
+ };
26
+
27
+ /**
28
+ * Synchronous listener for context events
29
+ */
30
+ export type ContextEventListener = (event: ContextEvent) => void;
@@ -0,0 +1,50 @@
1
+ // Copyright IBM Corp. 2019,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 {Binding} from './binding';
7
+ import {BindingFilter} from './binding-filter';
8
+ import {Context} from './context';
9
+ import {ValueOrPromise} from './value-promise';
10
+
11
+ /**
12
+ * Context event types. We support `bind` and `unbind` for now but
13
+ * keep it open for new types
14
+ */
15
+ export type ContextEventType = 'bind' | 'unbind' | string;
16
+
17
+ /**
18
+ * Listen on `bind`, `unbind`, or other events
19
+ * @param eventType - Context event type
20
+ * @param binding - The binding as event source
21
+ * @param context - Context object for the binding event
22
+ */
23
+ export type ContextObserverFn = (
24
+ eventType: ContextEventType,
25
+ binding: Readonly<Binding<unknown>>,
26
+ context: Context,
27
+ ) => ValueOrPromise<void>;
28
+
29
+ /**
30
+ * Observers of context bind/unbind events
31
+ */
32
+ export interface ContextObserver {
33
+ /**
34
+ * An optional filter function to match bindings. If not present, the listener
35
+ * will be notified of all binding events.
36
+ */
37
+ filter?: BindingFilter;
38
+
39
+ /**
40
+ * Listen on `bind`, `unbind`, or other events
41
+ * @param eventType - Context event type
42
+ * @param binding - The binding as event source
43
+ */
44
+ observe: ContextObserverFn;
45
+ }
46
+
47
+ /**
48
+ * Context event observer type - An instance of `ContextObserver` or a function
49
+ */
50
+ export type ContextEventObserver = ContextObserver | ContextObserverFn;
@@ -0,0 +1,402 @@
1
+ // Copyright IBM Corp. 2019,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 from 'debug';
7
+ import {EventEmitter} from 'events';
8
+ import {iterator, multiple} from 'p-event';
9
+ import {Context} from './context';
10
+ import {ContextEvent, ContextEventListener} from './context-event';
11
+ import {
12
+ ContextEventObserver,
13
+ ContextEventType,
14
+ ContextObserver,
15
+ } from './context-observer';
16
+
17
+ const debug = debugFactory('loopback:context:subscription');
18
+
19
+ /**
20
+ * Subscription of context events. It's modeled after
21
+ * https://github.com/tc39/proposal-observable.
22
+ */
23
+ export interface Subscription {
24
+ /**
25
+ * unsubscribe
26
+ */
27
+ unsubscribe(): void;
28
+ /**
29
+ * Is the subscription closed?
30
+ */
31
+ closed: boolean;
32
+ }
33
+
34
+ /**
35
+ * Event data for observer notifications
36
+ */
37
+ export interface Notification extends ContextEvent {
38
+ /**
39
+ * A snapshot of observers when the original event is emitted
40
+ */
41
+ observers: Set<ContextEventObserver>;
42
+ }
43
+
44
+ /**
45
+ * An implementation of `Subscription` interface for context events
46
+ */
47
+ class ContextSubscription implements Subscription {
48
+ constructor(
49
+ protected context: Context,
50
+ protected observer: ContextEventObserver,
51
+ ) {}
52
+
53
+ private _closed = false;
54
+
55
+ unsubscribe() {
56
+ this.context.unsubscribe(this.observer);
57
+ this._closed = true;
58
+ }
59
+
60
+ get closed() {
61
+ return this._closed;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Manager for context observer subscriptions
67
+ */
68
+ export class ContextSubscriptionManager extends EventEmitter {
69
+ /**
70
+ * A listener to watch parent context events
71
+ */
72
+ protected _parentContextEventListener?: ContextEventListener;
73
+
74
+ /**
75
+ * A list of registered context observers. The Set will be created when the
76
+ * first observer is added.
77
+ */
78
+ protected _observers: Set<ContextEventObserver> | undefined;
79
+
80
+ /**
81
+ * Internal counter for pending notification events which are yet to be
82
+ * processed by observers.
83
+ */
84
+ private pendingNotifications = 0;
85
+
86
+ /**
87
+ * Queue for background notifications for observers
88
+ */
89
+ private notificationQueue: AsyncIterableIterator<Notification> | undefined;
90
+
91
+ constructor(protected readonly context: Context) {
92
+ super();
93
+ this.setMaxListeners(Infinity);
94
+ }
95
+
96
+ /**
97
+ * @internal
98
+ */
99
+ get parentContextEventListener() {
100
+ return this._parentContextEventListener;
101
+ }
102
+
103
+ /**
104
+ * @internal
105
+ */
106
+ get observers() {
107
+ return this._observers;
108
+ }
109
+
110
+ /**
111
+ * Wrap the debug statement so that it always print out the context name
112
+ * as the prefix
113
+ * @param args - Arguments for the debug
114
+ */
115
+ private _debug(...args: unknown[]) {
116
+ /* istanbul ignore if */
117
+ if (!debug.enabled) return;
118
+ const formatter = args.shift();
119
+ if (typeof formatter === 'string') {
120
+ debug(`[%s] ${formatter}`, this.context.name, ...args);
121
+ } else {
122
+ debug('[%s] ', this.context.name, formatter, ...args);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Set up an internal listener to notify registered observers asynchronously
128
+ * upon `bind` and `unbind` events. This method will be called lazily when
129
+ * the first observer is added.
130
+ */
131
+ private setupEventHandlersIfNeeded() {
132
+ if (this.notificationQueue != null) return;
133
+
134
+ if (this.context.parent != null) {
135
+ /**
136
+ * Add an event listener to its parent context so that this context will
137
+ * be notified of parent events, such as `bind` or `unbind`.
138
+ */
139
+ this._parentContextEventListener = event => {
140
+ this.handleParentEvent(event);
141
+ };
142
+
143
+ // Listen on the parent context events
144
+ this.context.parent.on('bind', this._parentContextEventListener!);
145
+ this.context.parent.on('unbind', this._parentContextEventListener!);
146
+ }
147
+
148
+ // The following are two async functions. Returned promises are ignored as
149
+ // they are long-running background tasks.
150
+ this.startNotificationTask().catch(err => {
151
+ this.handleNotificationError(err);
152
+ });
153
+
154
+ let ctx = this.context.parent;
155
+ while (ctx) {
156
+ ctx.subscriptionManager.setupEventHandlersIfNeeded();
157
+ ctx = ctx.parent;
158
+ }
159
+ }
160
+
161
+ private handleParentEvent(event: ContextEvent) {
162
+ const {binding, context, type} = event;
163
+ // Propagate the event to this context only if the binding key does not
164
+ // exist in this context. The parent binding is shadowed if there is a
165
+ // binding with the same key in this one.
166
+ if (this.context.contains(binding.key)) {
167
+ this._debug(
168
+ 'Event %s %s is not re-emitted from %s to %s',
169
+ type,
170
+ binding.key,
171
+ context.name,
172
+ this.context.name,
173
+ );
174
+ return;
175
+ }
176
+ this._debug(
177
+ 'Re-emitting %s %s from %s to %s',
178
+ type,
179
+ binding.key,
180
+ context.name,
181
+ this.context.name,
182
+ );
183
+ this.context.emitEvent(type, event);
184
+ }
185
+
186
+ /**
187
+ * A strongly-typed method to emit context events
188
+ * @param type Event type
189
+ * @param event Context event
190
+ */
191
+ private emitEvent<T extends ContextEvent>(type: string, event: T) {
192
+ this.emit(type, event);
193
+ }
194
+
195
+ /**
196
+ * Emit an `error` event
197
+ * @param err Error
198
+ */
199
+ private emitError(err: unknown) {
200
+ this.emit('error', err);
201
+ }
202
+
203
+ /**
204
+ * Start a background task to listen on context events and notify observers
205
+ */
206
+ private startNotificationTask() {
207
+ // Set up listeners on `bind` and `unbind` for notifications
208
+ this.setupNotification('bind', 'unbind');
209
+
210
+ // Create an async iterator for the `notification` event as a queue
211
+ this.notificationQueue = iterator(this, 'notification', {
212
+ // Do not end the iterator if an error event is emitted on the
213
+ // subscription manager
214
+ rejectionEvents: [],
215
+ });
216
+
217
+ return this.processNotifications();
218
+ }
219
+
220
+ /**
221
+ * Publish an event to the registered observers. Please note the
222
+ * notification is queued and performed asynchronously so that we allow fluent
223
+ * APIs such as `ctx.bind('key').to(...).tag(...);` and give observers the
224
+ * fully populated binding.
225
+ *
226
+ * @param event - Context event
227
+ * @param observers - Current set of context observers
228
+ */
229
+ protected async notifyObservers(
230
+ event: ContextEvent,
231
+ observers = this._observers,
232
+ ) {
233
+ if (!observers || observers.size === 0) return;
234
+
235
+ const {type, binding, context} = event;
236
+ for (const observer of observers) {
237
+ if (typeof observer === 'function') {
238
+ await observer(type, binding, context);
239
+ } else if (!observer.filter || observer.filter(binding)) {
240
+ await observer.observe(type, binding, context);
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Process notification events as they arrive on the queue
247
+ */
248
+ private async processNotifications() {
249
+ const events = this.notificationQueue;
250
+ if (events == null) return;
251
+ for await (const {type, binding, context, observers} of events) {
252
+ // The loop will happen asynchronously upon events
253
+ try {
254
+ // The execution of observers happen in the Promise micro-task queue
255
+ await this.notifyObservers({type, binding, context}, observers);
256
+ this.pendingNotifications--;
257
+ this._debug(
258
+ 'Observers notified for %s of binding %s',
259
+ type,
260
+ binding.key,
261
+ );
262
+ this.emitEvent('observersNotified', {type, binding, context});
263
+ } catch (err) {
264
+ // Do not reduce the pending notification count so that errors
265
+ // can be captured by waitUntilPendingNotificationsDone
266
+ this._debug('Error caught from observers', err);
267
+ // Errors caught from observers.
268
+ if (this.listenerCount('error') > 0) {
269
+ // waitUntilPendingNotificationsDone may be called
270
+ this.emitError(err);
271
+ } else {
272
+ // Emit it to the current context. If no error listeners are
273
+ // registered, crash the process.
274
+ this.handleNotificationError(err);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Listen on given event types and emit `notification` event. This method
282
+ * merge multiple event types into one for notification.
283
+ * @param eventTypes - Context event types
284
+ */
285
+ private setupNotification(...eventTypes: ContextEventType[]) {
286
+ for (const type of eventTypes) {
287
+ this.context.on(type, ({binding, context}) => {
288
+ // No need to schedule notifications if no observers are present
289
+ if (!this._observers || this._observers.size === 0) return;
290
+ // Track pending events
291
+ this.pendingNotifications++;
292
+ // Take a snapshot of current observers to ensure notifications of this
293
+ // event will only be sent to current ones. Emit a new event to notify
294
+ // current context observers.
295
+ this.emitEvent('notification', {
296
+ type,
297
+ binding,
298
+ context,
299
+ observers: new Set(this._observers),
300
+ });
301
+ });
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Wait until observers are notified for all of currently pending notification
307
+ * events.
308
+ *
309
+ * This method is for test only to perform assertions after observers are
310
+ * notified for relevant events.
311
+ */
312
+ async waitUntilPendingNotificationsDone(timeout?: number) {
313
+ const count = this.pendingNotifications;
314
+ debug('Number of pending notifications: %d', count);
315
+ if (count === 0) return;
316
+ await multiple(this, 'observersNotified', {count, timeout});
317
+ }
318
+
319
+ /**
320
+ * Add a context event observer to the context
321
+ * @param observer - Context observer instance or function
322
+ */
323
+ subscribe(observer: ContextEventObserver): Subscription {
324
+ this._observers = this._observers ?? new Set();
325
+ this.setupEventHandlersIfNeeded();
326
+ this._observers.add(observer);
327
+ return new ContextSubscription(this.context, observer);
328
+ }
329
+
330
+ /**
331
+ * Remove the context event observer from the context
332
+ * @param observer - Context event observer
333
+ */
334
+ unsubscribe(observer: ContextEventObserver): boolean {
335
+ if (!this._observers) return false;
336
+ return this._observers.delete(observer);
337
+ }
338
+
339
+ /**
340
+ * Check if an observer is subscribed to this context
341
+ * @param observer - Context observer
342
+ */
343
+ isSubscribed(observer: ContextObserver) {
344
+ if (!this._observers) return false;
345
+ return this._observers.has(observer);
346
+ }
347
+
348
+ /**
349
+ * Handle errors caught during the notification of observers
350
+ * @param err - Error
351
+ */
352
+ private handleNotificationError(err: unknown) {
353
+ // Bubbling up the error event over the context chain
354
+ // until we find an error listener
355
+ let ctx: Context | undefined = this.context;
356
+ while (ctx) {
357
+ if (ctx.listenerCount('error') === 0) {
358
+ // No error listener found, try its parent
359
+ ctx = ctx.parent;
360
+ continue;
361
+ }
362
+ this._debug('Emitting error to context %s', ctx.name, err);
363
+ ctx.emitError(err);
364
+ return;
365
+ }
366
+ // No context with error listeners found
367
+ this._debug('No error handler is configured for the context chain', err);
368
+ // Let it crash now by emitting an error event
369
+ this.context.emitError(err);
370
+ }
371
+
372
+ /**
373
+ * Close the context: clear observers, stop notifications, and remove event
374
+ * listeners from its parent context.
375
+ *
376
+ * @remarks
377
+ * This method MUST be called to avoid memory leaks once a context object is
378
+ * no longer needed and should be recycled. An example is the `RequestContext`,
379
+ * which is created per request.
380
+ */
381
+ close() {
382
+ this._observers = undefined;
383
+ if (this.notificationQueue != null) {
384
+ // Cancel the notification iterator
385
+ this.notificationQueue.return!(undefined).catch(err => {
386
+ this.handleNotificationError(err);
387
+ });
388
+ this.notificationQueue = undefined;
389
+ }
390
+ if (this.context.parent && this._parentContextEventListener) {
391
+ this.context.parent.removeListener(
392
+ 'bind',
393
+ this._parentContextEventListener,
394
+ );
395
+ this.context.parent.removeListener(
396
+ 'unbind',
397
+ this._parentContextEventListener,
398
+ );
399
+ this._parentContextEventListener = undefined;
400
+ }
401
+ }
402
+ }
@@ -0,0 +1,147 @@
1
+ // Copyright IBM Corp. 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 {Binding, BindingEventListener, BindingTag} from './binding';
7
+ import {BindingFilter, filterByTag} from './binding-filter';
8
+ import {Context} from './context';
9
+ import {ContextEventListener} from './context-event';
10
+ import {BoundValue} from './value-promise';
11
+
12
+ /**
13
+ * Indexer for context bindings by tag
14
+ */
15
+ export class ContextTagIndexer {
16
+ /**
17
+ * Index for bindings by tag names
18
+ */
19
+ readonly bindingsIndexedByTag: Map<string, Set<Readonly<Binding<unknown>>>> =
20
+ new Map();
21
+
22
+ /**
23
+ * A listener for binding events
24
+ */
25
+ private bindingEventListener: BindingEventListener;
26
+
27
+ /**
28
+ * A listener to maintain tag index for bindings
29
+ */
30
+ private tagIndexListener: ContextEventListener;
31
+
32
+ constructor(protected readonly context: Context) {
33
+ this.setupTagIndexForBindings();
34
+ }
35
+
36
+ /**
37
+ * Set up context/binding listeners and refresh index for bindings by tag
38
+ */
39
+ private setupTagIndexForBindings() {
40
+ this.bindingEventListener = ({binding, operation}) => {
41
+ if (operation === 'tag') {
42
+ this.updateTagIndexForBinding(binding);
43
+ }
44
+ };
45
+ this.tagIndexListener = event => {
46
+ const {binding, type} = event;
47
+ if (event.context !== this.context) return;
48
+ if (type === 'bind') {
49
+ this.updateTagIndexForBinding(binding);
50
+ binding.on('changed', this.bindingEventListener);
51
+ } else if (type === 'unbind') {
52
+ this.removeTagIndexForBinding(binding);
53
+ binding.removeListener('changed', this.bindingEventListener);
54
+ }
55
+ };
56
+ this.context.on('bind', this.tagIndexListener);
57
+ this.context.on('unbind', this.tagIndexListener);
58
+ }
59
+
60
+ /**
61
+ * Remove tag index for the given binding
62
+ * @param binding - Binding object
63
+ */
64
+ private removeTagIndexForBinding(binding: Readonly<Binding<unknown>>) {
65
+ for (const [, bindings] of this.bindingsIndexedByTag) {
66
+ bindings.delete(binding);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Update tag index for the given binding
72
+ * @param binding - Binding object
73
+ */
74
+ private updateTagIndexForBinding(binding: Readonly<Binding<unknown>>) {
75
+ this.removeTagIndexForBinding(binding);
76
+ for (const tag of binding.tagNames) {
77
+ let bindings = this.bindingsIndexedByTag.get(tag);
78
+ if (bindings == null) {
79
+ bindings = new Set();
80
+ this.bindingsIndexedByTag.set(tag, bindings);
81
+ }
82
+ bindings.add(binding);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Find bindings by tag leveraging indexes
88
+ * @param tag - Tag name pattern or name/value pairs
89
+ */
90
+ findByTagIndex<ValueType = BoundValue>(
91
+ tag: BindingTag | RegExp,
92
+ ): Readonly<Binding<ValueType>>[] {
93
+ let tagNames: string[];
94
+ // A flag to control if a union of matched bindings should be created
95
+ let union = false;
96
+ if (tag instanceof RegExp) {
97
+ // For wildcard/regexp, a union of matched bindings is desired
98
+ union = true;
99
+ // Find all matching tag names
100
+ tagNames = [];
101
+ for (const t of this.bindingsIndexedByTag.keys()) {
102
+ if (tag.test(t)) {
103
+ tagNames.push(t);
104
+ }
105
+ }
106
+ } else if (typeof tag === 'string') {
107
+ tagNames = [tag];
108
+ } else {
109
+ tagNames = Object.keys(tag);
110
+ }
111
+ let filter: BindingFilter | undefined;
112
+ let bindings: Set<Readonly<Binding<ValueType>>> | undefined;
113
+ for (const t of tagNames) {
114
+ const bindingsByTag = this.bindingsIndexedByTag.get(t);
115
+ if (bindingsByTag == null) break; // One of the tags is not found
116
+ filter = filter ?? filterByTag(tag);
117
+ const matched = new Set(Array.from(bindingsByTag).filter(filter)) as Set<
118
+ Readonly<Binding<ValueType>>
119
+ >;
120
+ if (!union && matched.size === 0) break; // One of the tag name/value is not found
121
+ if (bindings == null) {
122
+ // First set of bindings matching the tag
123
+ bindings = matched;
124
+ } else {
125
+ if (union) {
126
+ matched.forEach(b => bindings?.add(b));
127
+ } else {
128
+ // Now need to find intersected bindings against visited tags
129
+ const intersection = new Set<Readonly<Binding<ValueType>>>();
130
+ bindings.forEach(b => {
131
+ if (matched.has(b)) {
132
+ intersection.add(b);
133
+ }
134
+ });
135
+ bindings = intersection;
136
+ }
137
+ if (!union && bindings.size === 0) break;
138
+ }
139
+ }
140
+ return bindings == null ? [] : Array.from(bindings);
141
+ }
142
+
143
+ close() {
144
+ this.context.removeListener('bind', this.tagIndexListener);
145
+ this.context.removeListener('unbind', this.tagIndexListener);
146
+ }
147
+ }