@loopback/context 1.25.1 → 2.0.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 (45) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/binding-filter.d.ts +19 -1
  3. package/dist/binding-filter.js +40 -7
  4. package/dist/binding-filter.js.map +1 -1
  5. package/dist/binding.d.ts +33 -1
  6. package/dist/binding.js +14 -1
  7. package/dist/binding.js.map +1 -1
  8. package/dist/context-event.d.ts +23 -0
  9. package/dist/context-event.js +7 -0
  10. package/dist/context-event.js.map +1 -0
  11. package/dist/context-observer.d.ts +1 -36
  12. package/dist/context-subscription.d.ts +147 -0
  13. package/dist/context-subscription.js +336 -0
  14. package/dist/context-subscription.js.map +1 -0
  15. package/dist/context-tag-indexer.d.ts +42 -0
  16. package/dist/context-tag-indexer.js +134 -0
  17. package/dist/context-tag-indexer.js.map +1 -0
  18. package/dist/context-view.d.ts +2 -1
  19. package/dist/context-view.js.map +1 -1
  20. package/dist/context.d.ts +29 -65
  21. package/dist/context.js +50 -245
  22. package/dist/context.js.map +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/inject.d.ts +1 -1
  27. package/dist/interceptor.js +4 -4
  28. package/dist/interceptor.js.map +1 -1
  29. package/dist/invocation.d.ts +0 -1
  30. package/dist/invocation.js +1 -5
  31. package/dist/invocation.js.map +1 -1
  32. package/dist/value-promise.d.ts +1 -3
  33. package/package.json +7 -7
  34. package/src/binding-filter.ts +61 -9
  35. package/src/binding.ts +43 -1
  36. package/src/context-event.ts +30 -0
  37. package/src/context-observer.ts +1 -38
  38. package/src/context-subscription.ts +403 -0
  39. package/src/context-tag-indexer.ts +149 -0
  40. package/src/context-view.ts +2 -5
  41. package/src/context.ts +71 -286
  42. package/src/index.ts +2 -0
  43. package/src/interceptor.ts +7 -6
  44. package/src/invocation.ts +1 -3
  45. package/src/value-promise.ts +1 -1
package/src/context.ts CHANGED
@@ -11,16 +11,18 @@ import {
11
11
  ConfigurationResolver,
12
12
  DefaultConfigurationResolver,
13
13
  } from './binding-config';
14
- import {BindingFilter, filterByKey, filterByTag} from './binding-filter';
14
+ import {
15
+ BindingFilter,
16
+ filterByKey,
17
+ filterByTag,
18
+ isBindingTagFilter,
19
+ } from './binding-filter';
15
20
  import {BindingAddress, BindingKey} from './binding-key';
16
21
  import {BindingComparator} from './binding-sorter';
17
- import {
18
- ContextEventObserver,
19
- ContextEventType,
20
- ContextObserver,
21
- Notification,
22
- Subscription,
23
- } from './context-observer';
22
+ import {ContextEvent} from './context-event';
23
+ import {ContextEventObserver, ContextObserver} from './context-observer';
24
+ import {ContextSubscriptionManager, Subscription} from './context-subscription';
25
+ import {ContextTagIndexer} from './context-tag-indexer';
24
26
  import {ContextView} from './context-view';
25
27
  import {ContextBindings} from './keys';
26
28
  import {
@@ -36,21 +38,6 @@ import {
36
38
  ValueOrPromise,
37
39
  } from './value-promise';
38
40
 
39
- /**
40
- * Polyfill Symbol.asyncIterator as required by TypeScript for Node 8.x.
41
- * See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html
42
- */
43
- if (!Symbol.asyncIterator) {
44
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
- (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator');
46
- }
47
- /**
48
- * WARNING: This following import must happen after the polyfill. The
49
- * `auto-import` by an IDE such as VSCode may move the import before the
50
- * polyfill. It must be then fixed manually.
51
- */
52
- import {iterator, multiple} from 'p-event';
53
-
54
41
  const debug = debugFactory('loopback:context');
55
42
 
56
43
  /**
@@ -68,41 +55,24 @@ export class Context extends EventEmitter {
68
55
  protected readonly registry: Map<string, Binding> = new Map();
69
56
 
70
57
  /**
71
- * Parent context
72
- */
73
- protected _parent?: Context;
74
-
75
- protected configResolver: ConfigurationResolver;
76
-
77
- /**
78
- * Event listeners for parent context keyed by event names. It keeps track
79
- * of listeners from this context against its parent so that we can remove
80
- * these listeners when this context is closed.
58
+ * Indexer for bindings by tag
81
59
  */
82
- protected _parentEventListeners:
83
- | Map<
84
- string,
85
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- (...args: any[]) => void
87
- >
88
- | undefined;
60
+ protected readonly tagIndexer: ContextTagIndexer;
89
61
 
90
62
  /**
91
- * A list of registered context observers. The Set will be created when the
92
- * first observer is added.
63
+ * Manager for observer subscriptions
93
64
  */
94
- protected observers: Set<ContextEventObserver> | undefined;
65
+ readonly subscriptionManager: ContextSubscriptionManager;
95
66
 
96
67
  /**
97
- * Internal counter for pending notification events which are yet to be
98
- * processed by observers.
68
+ * Parent context
99
69
  */
100
- private pendingNotifications = 0;
70
+ protected _parent?: Context;
101
71
 
102
72
  /**
103
- * Queue for background notifications for observers
73
+ * Configuration resolver
104
74
  */
105
- private notificationQueue: AsyncIterableIterator<Notification> | undefined;
75
+ protected configResolver: ConfigurationResolver;
106
76
 
107
77
  /**
108
78
  * Create a new context.
@@ -128,12 +98,27 @@ export class Context extends EventEmitter {
128
98
  */
129
99
  constructor(_parent?: Context | string, name?: string) {
130
100
  super();
101
+ // The number of listeners can grow with the number of child contexts
102
+ // For example, each request can add a listener to the RestServer and the
103
+ // listener is removed when the request processing is finished.
104
+ // See https://github.com/strongloop/loopback-next/issues/4363
105
+ this.setMaxListeners(Infinity);
131
106
  if (typeof _parent === 'string') {
132
107
  name = _parent;
133
108
  _parent = undefined;
134
109
  }
135
110
  this._parent = _parent;
136
111
  this.name = name ?? uuidv1();
112
+ this.tagIndexer = new ContextTagIndexer(this);
113
+ this.subscriptionManager = new ContextSubscriptionManager(this);
114
+ }
115
+
116
+ /**
117
+ * @internal
118
+ * Getter for ContextSubscriptionManager
119
+ */
120
+ get parent() {
121
+ return this._parent;
137
122
  }
138
123
 
139
124
  /**
@@ -141,8 +126,7 @@ export class Context extends EventEmitter {
141
126
  * as the prefix
142
127
  * @param args - Arguments for the debug
143
128
  */
144
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
- private _debug(...args: any[]) {
129
+ private _debug(...args: unknown[]) {
146
130
  /* istanbul ignore if */
147
131
  if (!debug.enabled) return;
148
132
  const formatter = args.shift();
@@ -154,176 +138,22 @@ export class Context extends EventEmitter {
154
138
  }
155
139
 
156
140
  /**
157
- * Set up an internal listener to notify registered observers asynchronously
158
- * upon `bind` and `unbind` events. This method will be called lazily when
159
- * the first observer is added.
141
+ * A strongly-typed method to emit context events
142
+ * @param type Event type
143
+ * @param event Context event
160
144
  */
161
- private setupEventHandlersIfNeeded() {
162
- if (this.notificationQueue != null) return;
163
-
164
- this.addParentEventListener('bind');
165
- this.addParentEventListener('unbind');
166
-
167
- // The following are two async functions. Returned promises are ignored as
168
- // they are long-running background tasks.
169
- this.startNotificationTask().catch(err => {
170
- this.handleNotificationError(err);
171
- });
172
-
173
- let ctx = this._parent;
174
- while (ctx) {
175
- ctx.setupEventHandlersIfNeeded();
176
- ctx = ctx._parent;
177
- }
145
+ emitEvent<T extends ContextEvent>(type: string, event: T) {
146
+ this.emit(type, event);
178
147
  }
179
148
 
180
149
  /**
181
- * Add an event listener to its parent context so that this context will
182
- * be notified of parent events, such as `bind` or `unbind`.
183
- * @param event - Event name
150
+ * Emit an `error` event
151
+ * @param err Error
184
152
  */
185
- private addParentEventListener(event: string) {
186
- if (this._parent == null) return;
187
-
188
- // Keep track of parent event listeners so that we can remove them
189
- this._parentEventListeners = this._parentEventListeners ?? new Map();
190
- if (this._parentEventListeners.has(event)) return;
191
-
192
- const parentEventListener = (
193
- binding: Readonly<Binding<unknown>>,
194
- context: Context,
195
- ) => {
196
- // Propagate the event to this context only if the binding key does not
197
- // exist in this context. The parent binding is shadowed if there is a
198
- // binding with the same key in this one.
199
- if (this.contains(binding.key)) {
200
- this._debug(
201
- 'Event %s %s is not re-emitted from %s to %s',
202
- event,
203
- binding.key,
204
- context.name,
205
- this.name,
206
- );
207
- return;
208
- }
209
- this._debug(
210
- 'Re-emitting %s %s from %s to %s',
211
- event,
212
- binding.key,
213
- context.name,
214
- this.name,
215
- );
216
- this.emit(event, binding, context);
217
- };
218
- this._parentEventListeners.set(event, parentEventListener);
219
- // Listen on the parent context events
220
- this._parent.on(event, parentEventListener);
221
- }
222
-
223
- /**
224
- * Handle errors caught during the notification of observers
225
- * @param err - Error
226
- */
227
- private handleNotificationError(err: unknown) {
228
- // Bubbling up the error event over the context chain
229
- // until we find an error listener
230
- // eslint-disable-next-line @typescript-eslint/no-this-alias
231
- let ctx: Context | undefined = this;
232
- while (ctx) {
233
- if (ctx.listenerCount('error') === 0) {
234
- // No error listener found, try its parent
235
- ctx = ctx._parent;
236
- continue;
237
- }
238
- this._debug('Emitting error to context %s', ctx.name, err);
239
- ctx.emit('error', err);
240
- return;
241
- }
242
- // No context with error listeners found
243
- this._debug('No error handler is configured for the context chain', err);
244
- // Let it crash now by emitting an error event
153
+ emitError(err: unknown) {
245
154
  this.emit('error', err);
246
155
  }
247
156
 
248
- /**
249
- * Start a background task to listen on context events and notify observers
250
- */
251
- private startNotificationTask() {
252
- // Set up listeners on `bind` and `unbind` for notifications
253
- this.setupNotification('bind', 'unbind');
254
-
255
- // Create an async iterator for the `notification` event as a queue
256
- this.notificationQueue = iterator(this, 'notification');
257
-
258
- return this.processNotifications();
259
- }
260
-
261
- /**
262
- * Process notification events as they arrive on the queue
263
- */
264
- private async processNotifications() {
265
- const events = this.notificationQueue;
266
- if (events == null) return;
267
- for await (const {eventType, binding, context, observers} of events) {
268
- // The loop will happen asynchronously upon events
269
- try {
270
- // The execution of observers happen in the Promise micro-task queue
271
- await this.notifyObservers(eventType, binding, context, observers);
272
- this.pendingNotifications--;
273
- this._debug(
274
- 'Observers notified for %s of binding %s',
275
- eventType,
276
- binding.key,
277
- );
278
- this.emit('observersNotified', {eventType, binding});
279
- } catch (err) {
280
- this.pendingNotifications--;
281
- this._debug('Error caught from observers', err);
282
- // Errors caught from observers. Emit it to the current context.
283
- // If no error listeners are registered, crash the process.
284
- this.emit('error', err);
285
- }
286
- }
287
- }
288
-
289
- /**
290
- * Listen on given event types and emit `notification` event. This method
291
- * merge multiple event types into one for notification.
292
- * @param eventTypes - Context event types
293
- */
294
- private setupNotification(...eventTypes: ContextEventType[]) {
295
- for (const eventType of eventTypes) {
296
- this.on(eventType, (binding, context) => {
297
- // No need to schedule notifications if no observers are present
298
- if (!this.observers || this.observers.size === 0) return;
299
- // Track pending events
300
- this.pendingNotifications++;
301
- // Take a snapshot of current observers to ensure notifications of this
302
- // event will only be sent to current ones. Emit a new event to notify
303
- // current context observers.
304
- this.emit('notification', {
305
- eventType,
306
- binding,
307
- context,
308
- observers: new Set(this.observers),
309
- });
310
- });
311
- }
312
- }
313
-
314
- /**
315
- * Wait until observers are notified for all of currently pending notification
316
- * events.
317
- *
318
- * This method is for test only to perform assertions after observers are
319
- * notified for relevant events.
320
- */
321
- protected async waitUntilPendingNotificationsDone(timeout?: number) {
322
- const count = this.pendingNotifications;
323
- if (count === 0) return;
324
- await multiple(this, 'observersNotified', {count, timeout});
325
- }
326
-
327
157
  /**
328
158
  * Create a binding with the given key in the context. If a locked binding
329
159
  * already exists with the same key, an error will be thrown.
@@ -357,9 +187,13 @@ export class Context extends EventEmitter {
357
187
  this.registry.set(key, binding);
358
188
  if (existingBinding !== binding) {
359
189
  if (existingBinding != null) {
360
- this.emit('unbind', existingBinding, this);
190
+ this.emitEvent('unbind', {
191
+ binding: existingBinding,
192
+ context: this,
193
+ type: 'unbind',
194
+ });
361
195
  }
362
- this.emit('bind', binding, this);
196
+ this.emitEvent('bind', {binding, context: this, type: 'bind'});
363
197
  }
364
198
  return this;
365
199
  }
@@ -505,7 +339,7 @@ export class Context extends EventEmitter {
505
339
  if (binding?.isLocked)
506
340
  throw new Error(`Cannot unbind key "${key}" of a locked binding`);
507
341
  this.registry.delete(key);
508
- this.emit('unbind', binding, this);
342
+ this.emitEvent('unbind', {binding, context: this, type: 'unbind'});
509
343
  return true;
510
344
  }
511
345
 
@@ -514,10 +348,7 @@ export class Context extends EventEmitter {
514
348
  * @param observer - Context observer instance or function
515
349
  */
516
350
  subscribe(observer: ContextEventObserver): Subscription {
517
- this.observers = this.observers ?? new Set();
518
- this.setupEventHandlersIfNeeded();
519
- this.observers.add(observer);
520
- return new ContextSubscription(this, observer);
351
+ return this.subscriptionManager.subscribe(observer);
521
352
  }
522
353
 
523
354
  /**
@@ -525,8 +356,7 @@ export class Context extends EventEmitter {
525
356
  * @param observer - Context event observer
526
357
  */
527
358
  unsubscribe(observer: ContextEventObserver): boolean {
528
- if (!this.observers) return false;
529
- return this.observers.delete(observer);
359
+ return this.subscriptionManager.unsubscribe(observer);
530
360
  }
531
361
 
532
362
  /**
@@ -540,20 +370,8 @@ export class Context extends EventEmitter {
540
370
  */
541
371
  close() {
542
372
  this._debug('Closing context...');
543
- this.observers = undefined;
544
- if (this.notificationQueue != null) {
545
- // Cancel the notification iterator
546
- this.notificationQueue.return!(undefined).catch(err => {
547
- this.handleNotificationError(err);
548
- });
549
- this.notificationQueue = undefined;
550
- }
551
- if (this._parent && this._parentEventListeners) {
552
- for (const [event, listener] of this._parentEventListeners) {
553
- this._parent.removeListener(event, listener);
554
- }
555
- this._parentEventListeners = undefined;
556
- }
373
+ this.subscriptionManager.close();
374
+ this.tagIndexer.close();
557
375
  }
558
376
 
559
377
  /**
@@ -561,8 +379,7 @@ export class Context extends EventEmitter {
561
379
  * @param observer - Context observer
562
380
  */
563
381
  isSubscribed(observer: ContextObserver) {
564
- if (!this.observers) return false;
565
- return this.observers.has(observer);
382
+ return this.subscriptionManager.isSubscribed(observer);
566
383
  }
567
384
 
568
385
  /**
@@ -579,34 +396,6 @@ export class Context extends EventEmitter {
579
396
  return view;
580
397
  }
581
398
 
582
- /**
583
- * Publish an event to the registered observers. Please note the
584
- * notification is queued and performed asynchronously so that we allow fluent
585
- * APIs such as `ctx.bind('key').to(...).tag(...);` and give observers the
586
- * fully populated binding.
587
- *
588
- * @param eventType - Event names: `bind` or `unbind`
589
- * @param binding - Binding bound or unbound
590
- * @param context - Owner context
591
- * @param observers - Current set of context observers
592
- */
593
- protected async notifyObservers(
594
- eventType: ContextEventType,
595
- binding: Readonly<Binding<unknown>>,
596
- context: Context,
597
- observers = this.observers,
598
- ) {
599
- if (!observers || observers.size === 0) return;
600
-
601
- for (const observer of observers) {
602
- if (typeof observer === 'function') {
603
- await observer(eventType, binding, context);
604
- } else if (!observer.filter || observer.filter(binding)) {
605
- await observer.observe(eventType, binding, context);
606
- }
607
- }
608
- }
609
-
610
399
  /**
611
400
  * Check if a binding exists with the given key in the local context without
612
401
  * delegating to the parent context
@@ -658,6 +447,11 @@ export class Context extends EventEmitter {
658
447
  find<ValueType = BoundValue>(
659
448
  pattern?: string | RegExp | BindingFilter,
660
449
  ): Readonly<Binding<ValueType>>[] {
450
+ // Optimize if the binding filter is for tags
451
+ if (typeof pattern === 'function' && isBindingTagFilter(pattern)) {
452
+ return this._findByTagIndex(pattern.bindingTagPattern);
453
+ }
454
+
661
455
  const bindings: Readonly<Binding<ValueType>>[] = [];
662
456
  const filter = filterByKey(pattern);
663
457
 
@@ -689,6 +483,18 @@ export class Context extends EventEmitter {
689
483
  return this.find(filterByTag(tagFilter));
690
484
  }
691
485
 
486
+ /**
487
+ * Find bindings by tag leveraging indexes
488
+ * @param tag - Tag name pattern or name/value pairs
489
+ */
490
+ protected _findByTagIndex<ValueType = BoundValue>(
491
+ tag: BindingTag | RegExp,
492
+ ): Readonly<Binding<ValueType>>[] {
493
+ const currentBindings = this.tagIndexer.findByTagIndex(tag);
494
+ const parentBindings = this._parent && this._parent?._findByTagIndex(tag);
495
+ return this._mergeWithParent(currentBindings, parentBindings);
496
+ }
497
+
692
498
  protected _mergeWithParent<ValueType>(
693
499
  childList: Readonly<Binding<ValueType>>[],
694
500
  parentList?: Readonly<Binding<ValueType>>[],
@@ -993,27 +799,6 @@ export class Context extends EventEmitter {
993
799
  }
994
800
  }
995
801
 
996
- /**
997
- * An implementation of `Subscription` interface for context events
998
- */
999
- class ContextSubscription implements Subscription {
1000
- constructor(
1001
- protected context: Context,
1002
- protected observer: ContextEventObserver,
1003
- ) {}
1004
-
1005
- private _closed = false;
1006
-
1007
- unsubscribe() {
1008
- this.context.unsubscribe(this.observer);
1009
- this._closed = true;
1010
- }
1011
-
1012
- get closed() {
1013
- return this._closed;
1014
- }
1015
- }
1016
-
1017
802
  /**
1018
803
  * Policy to control if a binding should be created for the context
1019
804
  */
package/src/index.ts CHANGED
@@ -12,7 +12,9 @@ export * from './binding-inspector';
12
12
  export * from './binding-key';
13
13
  export * from './binding-sorter';
14
14
  export * from './context';
15
+ export * from './context-event';
15
16
  export * from './context-observer';
17
+ export * from './context-subscription';
16
18
  export * from './context-view';
17
19
  export * from './inject';
18
20
  export * from './inject-config';
@@ -15,7 +15,6 @@ import assert from 'assert';
15
15
  import debugFactory from 'debug';
16
16
  import {Binding, BindingTemplate} from './binding';
17
17
  import {bind} from './binding-decorator';
18
- import {filterByTag} from './binding-filter';
19
18
  import {BindingSpec} from './binding-inspector';
20
19
  import {sortBindingsByPhase} from './binding-sorter';
21
20
  import {Context} from './context';
@@ -47,12 +46,14 @@ export class InterceptedInvocationContext extends InvocationContext {
47
46
  * ContextTags.GLOBAL_INTERCEPTOR)
48
47
  */
49
48
  getGlobalInterceptorBindingKeys(): string[] {
50
- const bindings: Readonly<Binding<Interceptor>>[] = this.find(
51
- binding =>
52
- filterByTag(ContextTags.GLOBAL_INTERCEPTOR)(binding) &&
53
- // Only include interceptors that match the source type of the invocation
54
- this.applicableTo(binding),
49
+ let bindings: Readonly<Binding<Interceptor>>[] = this.findByTag(
50
+ ContextTags.GLOBAL_INTERCEPTOR,
55
51
  );
52
+ bindings = bindings.filter(binding =>
53
+ // Only include interceptors that match the source type of the invocation
54
+ this.applicableTo(binding),
55
+ );
56
+
56
57
  this.sortGlobalInterceptorBindings(bindings);
57
58
  const keys = bindings.map(b => b.key);
58
59
  debug('Global interceptor binding keys:', keys);
package/src/invocation.ts CHANGED
@@ -55,9 +55,7 @@ export class InvocationContext extends Context {
55
55
  * @param args - An array of arguments
56
56
  */
57
57
  constructor(
58
- // Make `parent` public so that the interceptor can add bindings to
59
- // the request context, for example, tracing id
60
- public readonly parent: Context,
58
+ parent: Context,
61
59
  public readonly target: object,
62
60
  public readonly methodName: string,
63
61
  public readonly args: InvocationArgs,
@@ -29,7 +29,7 @@ export type BoundValue = any;
29
29
  */
30
30
  export type ValueOrPromise<T> = T | PromiseLike<T>;
31
31
 
32
- export type MapObject<T> = {[name: string]: T};
32
+ export type MapObject<T> = Record<string, T>;
33
33
 
34
34
  /**
35
35
  * Check whether a value is a Promise-like instance.