@loopback/context 1.25.0 → 2.1.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 (56) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/binding-filter.d.ts +20 -2
  3. package/dist/binding-filter.js +58 -9
  4. package/dist/binding-filter.js.map +1 -1
  5. package/dist/binding.d.ts +50 -2
  6. package/dist/binding.js +33 -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 +44 -68
  21. package/dist/context.js +69 -249
  22. package/dist/context.js.map +1 -1
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/inject.d.ts +9 -2
  27. package/dist/inject.js +60 -0
  28. package/dist/inject.js.map +1 -1
  29. package/dist/interceptor.js +4 -4
  30. package/dist/interceptor.js.map +1 -1
  31. package/dist/invocation.d.ts +0 -1
  32. package/dist/invocation.js +1 -5
  33. package/dist/invocation.js.map +1 -1
  34. package/dist/json-types.d.ts +28 -0
  35. package/dist/json-types.js +7 -0
  36. package/dist/json-types.js.map +1 -0
  37. package/dist/resolution-session.d.ts +2 -6
  38. package/dist/resolution-session.js +0 -4
  39. package/dist/resolution-session.js.map +1 -1
  40. package/dist/value-promise.d.ts +1 -3
  41. package/package.json +9 -9
  42. package/src/binding-filter.ts +83 -11
  43. package/src/binding.ts +79 -3
  44. package/src/context-event.ts +30 -0
  45. package/src/context-observer.ts +1 -38
  46. package/src/context-subscription.ts +403 -0
  47. package/src/context-tag-indexer.ts +149 -0
  48. package/src/context-view.ts +2 -5
  49. package/src/context.ts +104 -290
  50. package/src/index.ts +3 -0
  51. package/src/inject.ts +65 -0
  52. package/src/interceptor.ts +7 -6
  53. package/src/invocation.ts +1 -3
  54. package/src/json-types.ts +35 -0
  55. package/src/resolution-session.ts +4 -7
  56. package/src/value-promise.ts +1 -1
package/src/binding.ts CHANGED
@@ -4,9 +4,12 @@
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  import debugFactory from 'debug';
7
+ import {EventEmitter} from 'events';
7
8
  import {BindingAddress, BindingKey} from './binding-key';
8
9
  import {Context} from './context';
10
+ import {inspectInjections} from './inject';
9
11
  import {createProxyWithInterceptors} from './interception-proxy';
12
+ import {JSONObject} from './json-types';
10
13
  import {ContextTags} from './keys';
11
14
  import {Provider} from './provider';
12
15
  import {
@@ -139,6 +142,34 @@ export type BindingTag = TagMap | string;
139
142
  */
140
143
  export type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
141
144
 
145
+ /**
146
+ * Information for a binding event
147
+ */
148
+ export type BindingEvent = {
149
+ /**
150
+ * Event type
151
+ */
152
+ type: string;
153
+ /**
154
+ * Source binding that emits the event
155
+ */
156
+ binding: Readonly<Binding<unknown>>;
157
+ /**
158
+ * Operation that triggers the event
159
+ */
160
+ operation: string;
161
+ };
162
+
163
+ /**
164
+ * Event listeners for binding events
165
+ */
166
+ export type BindingEventListener = (
167
+ /**
168
+ * Binding event
169
+ */
170
+ event: BindingEvent,
171
+ ) => void;
172
+
142
173
  type ValueGetter<T> = (
143
174
  ctx: Context,
144
175
  options: ResolutionOptions,
@@ -148,7 +179,7 @@ type ValueGetter<T> = (
148
179
  * Binding represents an entry in the `Context`. Each binding has a key and a
149
180
  * corresponding value getter.
150
181
  */
151
- export class Binding<T = BoundValue> {
182
+ export class Binding<T = BoundValue> extends EventEmitter {
152
183
  /**
153
184
  * Key of the binding
154
185
  */
@@ -181,6 +212,7 @@ export class Binding<T = BoundValue> {
181
212
 
182
213
  private _valueConstructor?: Constructor<T>;
183
214
  private _providerConstructor?: Constructor<Provider<T>>;
215
+ private _alias?: BindingAddress<T>;
184
216
 
185
217
  /**
186
218
  * For bindings bound via `toClass()`, this property contains the constructor
@@ -199,6 +231,7 @@ export class Binding<T = BoundValue> {
199
231
  }
200
232
 
201
233
  constructor(key: BindingAddress<T>, public isLocked: boolean = false) {
234
+ super();
202
235
  BindingKey.validate(key);
203
236
  this.key = key.toString();
204
237
  }
@@ -324,6 +357,15 @@ export class Binding<T = BoundValue> {
324
357
  return this;
325
358
  }
326
359
 
360
+ /**
361
+ * Emit a `changed` event
362
+ * @param operation - Operation that makes changes
363
+ */
364
+ private emitChangedEvent(operation: string) {
365
+ const event: BindingEvent = {binding: this, operation, type: 'changed'};
366
+ this.emit('changed', event);
367
+ }
368
+
327
369
  /**
328
370
  * Tag the binding with names or name/value objects. A tag has a name and
329
371
  * an optional value. If not supplied, the tag name is used as the value.
@@ -362,6 +404,7 @@ export class Binding<T = BoundValue> {
362
404
  Object.assign(this.tagMap, t);
363
405
  }
364
406
  }
407
+ this.emitChangedEvent('tag');
365
408
  return this;
366
409
  }
367
410
 
@@ -379,6 +422,7 @@ export class Binding<T = BoundValue> {
379
422
  inScope(scope: BindingScope): this {
380
423
  if (this._scope !== scope) this._clearCache();
381
424
  this._scope = scope;
425
+ this.emitChangedEvent('scope');
382
426
  return this;
383
427
  }
384
428
 
@@ -409,6 +453,7 @@ export class Binding<T = BoundValue> {
409
453
  }
410
454
  return getValue(ctx, options);
411
455
  };
456
+ this.emitChangedEvent('value');
412
457
  }
413
458
 
414
459
  /**
@@ -553,6 +598,7 @@ export class Binding<T = BoundValue> {
553
598
  debug('Bind %s to alias %s', this.key, keyWithPath);
554
599
  }
555
600
  this._type = BindingType.ALIAS;
601
+ this._alias = keyWithPath;
556
602
  this._setValueGetter((ctx, options) => {
557
603
  return ctx.getValueOrPromise(keyWithPath, options);
558
604
  });
@@ -591,8 +637,8 @@ export class Binding<T = BoundValue> {
591
637
  /**
592
638
  * Convert to a plain JSON object
593
639
  */
594
- toJSON(): object {
595
- const json: Record<string, unknown> = {
640
+ toJSON(): JSONObject {
641
+ const json: JSONObject = {
596
642
  key: this.key,
597
643
  scope: this.scope,
598
644
  tags: this.tagMap,
@@ -607,6 +653,26 @@ export class Binding<T = BoundValue> {
607
653
  if (this._providerConstructor != null) {
608
654
  json.providerConstructor = this._providerConstructor.name;
609
655
  }
656
+ if (this._alias != null) {
657
+ json.alias = this._alias.toString();
658
+ }
659
+ return json;
660
+ }
661
+
662
+ /**
663
+ * Inspect the binding to return a json representation of the binding information
664
+ * @param options - Options to control what information should be included
665
+ */
666
+ inspect(options: BindingInspectOptions = {}): JSONObject {
667
+ options = {
668
+ includeInjections: false,
669
+ ...options,
670
+ };
671
+ const json = this.toJSON();
672
+ if (options.includeInjections) {
673
+ const injections = inspectInjections(this);
674
+ if (Object.keys(injections).length) json.injections = injections;
675
+ }
610
676
  return json;
611
677
  }
612
678
 
@@ -641,6 +707,16 @@ export class Binding<T = BoundValue> {
641
707
  }
642
708
  }
643
709
 
710
+ /**
711
+ * Options for binding.inspect()
712
+ */
713
+ export interface BindingInspectOptions {
714
+ /**
715
+ * The flag to control if injections should be inspected
716
+ */
717
+ includeInjections?: boolean;
718
+ }
719
+
644
720
  function createInterceptionProxyFromInstance<T>(
645
721
  instOrPromise: ValueOrPromise<T>,
646
722
  context: Context,
@@ -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;
@@ -5,8 +5,8 @@
5
5
 
6
6
  import {Binding} from './binding';
7
7
  import {BindingFilter} from './binding-filter';
8
- import {ValueOrPromise} from './value-promise';
9
8
  import {Context} from './context';
9
+ import {ValueOrPromise} from './value-promise';
10
10
 
11
11
  /**
12
12
  * Context event types. We support `bind` and `unbind` for now but
@@ -48,40 +48,3 @@ export interface ContextObserver {
48
48
  * Context event observer type - An instance of `ContextObserver` or a function
49
49
  */
50
50
  export type ContextEventObserver = ContextObserver | ContextObserverFn;
51
-
52
- /**
53
- * Subscription of context events. It's modeled after
54
- * https://github.com/tc39/proposal-observable.
55
- */
56
- export interface Subscription {
57
- /**
58
- * unsubscribe
59
- */
60
- unsubscribe(): void;
61
- /**
62
- * Is the subscription closed?
63
- */
64
- closed: boolean;
65
- }
66
-
67
- /**
68
- * Event data for observer notifications
69
- */
70
- export type Notification = {
71
- /**
72
- * Context event type - bind/unbind
73
- */
74
- eventType: ContextEventType;
75
- /**
76
- * Binding added/removed
77
- */
78
- binding: Readonly<Binding<unknown>>;
79
- /**
80
- * Owner context for the binding
81
- */
82
- context: Context;
83
- /**
84
- * A snapshot of observers when the original event is emitted
85
- */
86
- observers: Set<ContextEventObserver>;
87
- };
@@ -0,0 +1,403 @@
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 debugFactory from 'debug';
7
+ import {EventEmitter} from 'events';
8
+ import {Context} from './context';
9
+ import {ContextEvent, ContextEventListener} from './context-event';
10
+ import {
11
+ ContextEventObserver,
12
+ ContextEventType,
13
+ ContextObserver,
14
+ } from './context-observer';
15
+ const debug = debugFactory('loopback:context:subscription');
16
+
17
+ /**
18
+ * Polyfill Symbol.asyncIterator as required by TypeScript for Node 8.x.
19
+ * See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html
20
+ */
21
+ if (!Symbol.asyncIterator) {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator');
24
+ }
25
+ /**
26
+ * WARNING: This following import must happen after the polyfill. The
27
+ * `auto-import` by an IDE such as VSCode may move the import before the
28
+ * polyfill. It must be then fixed manually.
29
+ */
30
+ import {iterator, multiple} from 'p-event';
31
+
32
+ /**
33
+ * Subscription of context events. It's modeled after
34
+ * https://github.com/tc39/proposal-observable.
35
+ */
36
+ export interface Subscription {
37
+ /**
38
+ * unsubscribe
39
+ */
40
+ unsubscribe(): void;
41
+ /**
42
+ * Is the subscription closed?
43
+ */
44
+ closed: boolean;
45
+ }
46
+
47
+ /**
48
+ * Event data for observer notifications
49
+ */
50
+ export interface Notification extends ContextEvent {
51
+ /**
52
+ * A snapshot of observers when the original event is emitted
53
+ */
54
+ observers: Set<ContextEventObserver>;
55
+ }
56
+
57
+ /**
58
+ * An implementation of `Subscription` interface for context events
59
+ */
60
+ class ContextSubscription implements Subscription {
61
+ constructor(
62
+ protected context: Context,
63
+ protected observer: ContextEventObserver,
64
+ ) {}
65
+
66
+ private _closed = false;
67
+
68
+ unsubscribe() {
69
+ this.context.unsubscribe(this.observer);
70
+ this._closed = true;
71
+ }
72
+
73
+ get closed() {
74
+ return this._closed;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Manager for context observer subscriptions
80
+ */
81
+ export class ContextSubscriptionManager extends EventEmitter {
82
+ /**
83
+ * A listener to watch parent context events
84
+ */
85
+ protected _parentContextEventListener?: ContextEventListener;
86
+
87
+ /**
88
+ * A list of registered context observers. The Set will be created when the
89
+ * first observer is added.
90
+ */
91
+ protected _observers: Set<ContextEventObserver> | undefined;
92
+
93
+ /**
94
+ * Internal counter for pending notification events which are yet to be
95
+ * processed by observers.
96
+ */
97
+ private pendingNotifications = 0;
98
+
99
+ /**
100
+ * Queue for background notifications for observers
101
+ */
102
+ private notificationQueue: AsyncIterableIterator<Notification> | undefined;
103
+
104
+ constructor(protected readonly context: Context) {
105
+ super();
106
+ this.setMaxListeners(Infinity);
107
+ }
108
+
109
+ /**
110
+ * @internal
111
+ */
112
+ get parentContextEventListener() {
113
+ return this._parentContextEventListener;
114
+ }
115
+
116
+ /**
117
+ * @internal
118
+ */
119
+ get observers() {
120
+ return this._observers;
121
+ }
122
+
123
+ /**
124
+ * Wrap the debug statement so that it always print out the context name
125
+ * as the prefix
126
+ * @param args - Arguments for the debug
127
+ */
128
+ private _debug(...args: unknown[]) {
129
+ /* istanbul ignore if */
130
+ if (!debug.enabled) return;
131
+ const formatter = args.shift();
132
+ if (typeof formatter === 'string') {
133
+ debug(`[%s] ${formatter}`, this.context.name, ...args);
134
+ } else {
135
+ debug('[%s] ', this.context.name, formatter, ...args);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Set up an internal listener to notify registered observers asynchronously
141
+ * upon `bind` and `unbind` events. This method will be called lazily when
142
+ * the first observer is added.
143
+ */
144
+ private setupEventHandlersIfNeeded() {
145
+ if (this.notificationQueue != null) return;
146
+
147
+ if (this.context.parent != null) {
148
+ /**
149
+ * Add an event listener to its parent context so that this context will
150
+ * be notified of parent events, such as `bind` or `unbind`.
151
+ */
152
+ this._parentContextEventListener = event => {
153
+ this.handleParentEvent(event);
154
+ };
155
+
156
+ // Listen on the parent context events
157
+ this.context.parent.on('bind', this._parentContextEventListener!);
158
+ this.context.parent.on('unbind', this._parentContextEventListener!);
159
+ }
160
+
161
+ // The following are two async functions. Returned promises are ignored as
162
+ // they are long-running background tasks.
163
+ this.startNotificationTask().catch(err => {
164
+ this.handleNotificationError(err);
165
+ });
166
+
167
+ let ctx = this.context.parent;
168
+ while (ctx) {
169
+ ctx.subscriptionManager.setupEventHandlersIfNeeded();
170
+ ctx = ctx.parent;
171
+ }
172
+ }
173
+
174
+ private handleParentEvent(event: ContextEvent) {
175
+ const {binding, context, type} = event;
176
+ // Propagate the event to this context only if the binding key does not
177
+ // exist in this context. The parent binding is shadowed if there is a
178
+ // binding with the same key in this one.
179
+ if (this.context.contains(binding.key)) {
180
+ this._debug(
181
+ 'Event %s %s is not re-emitted from %s to %s',
182
+ type,
183
+ binding.key,
184
+ context.name,
185
+ this.context.name,
186
+ );
187
+ return;
188
+ }
189
+ this._debug(
190
+ 'Re-emitting %s %s from %s to %s',
191
+ type,
192
+ binding.key,
193
+ context.name,
194
+ this.context.name,
195
+ );
196
+ this.context.emitEvent(type, event);
197
+ }
198
+
199
+ /**
200
+ * A strongly-typed method to emit context events
201
+ * @param type Event type
202
+ * @param event Context event
203
+ */
204
+ private emitEvent<T extends ContextEvent>(type: string, event: T) {
205
+ this.emit(type, event);
206
+ }
207
+
208
+ /**
209
+ * Emit an `error` event
210
+ * @param err Error
211
+ */
212
+ private emitError(err: unknown) {
213
+ this.emit('error', err);
214
+ }
215
+
216
+ /**
217
+ * Start a background task to listen on context events and notify observers
218
+ */
219
+ private startNotificationTask() {
220
+ // Set up listeners on `bind` and `unbind` for notifications
221
+ this.setupNotification('bind', 'unbind');
222
+
223
+ // Create an async iterator for the `notification` event as a queue
224
+ this.notificationQueue = iterator(this, 'notification');
225
+
226
+ return this.processNotifications();
227
+ }
228
+
229
+ /**
230
+ * Publish an event to the registered observers. Please note the
231
+ * notification is queued and performed asynchronously so that we allow fluent
232
+ * APIs such as `ctx.bind('key').to(...).tag(...);` and give observers the
233
+ * fully populated binding.
234
+ *
235
+ * @param event - Context event
236
+ * @param observers - Current set of context observers
237
+ */
238
+ protected async notifyObservers(
239
+ event: ContextEvent,
240
+ observers = this._observers,
241
+ ) {
242
+ if (!observers || observers.size === 0) return;
243
+
244
+ const {type, binding, context} = event;
245
+ for (const observer of observers) {
246
+ if (typeof observer === 'function') {
247
+ await observer(type, binding, context);
248
+ } else if (!observer.filter || observer.filter(binding)) {
249
+ await observer.observe(type, binding, context);
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Process notification events as they arrive on the queue
256
+ */
257
+ private async processNotifications() {
258
+ const events = this.notificationQueue;
259
+ if (events == null) return;
260
+ for await (const {type, binding, context, observers} of events) {
261
+ // The loop will happen asynchronously upon events
262
+ try {
263
+ // The execution of observers happen in the Promise micro-task queue
264
+ await this.notifyObservers({type, binding, context}, observers);
265
+ this.pendingNotifications--;
266
+ this._debug(
267
+ 'Observers notified for %s of binding %s',
268
+ type,
269
+ binding.key,
270
+ );
271
+ this.emitEvent('observersNotified', {type, binding, context});
272
+ } catch (err) {
273
+ this.pendingNotifications--;
274
+ this._debug('Error caught from observers', err);
275
+ // Errors caught from observers. Emit it to the current context.
276
+ // If no error listeners are registered, crash the process.
277
+ this.emitError(err);
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Listen on given event types and emit `notification` event. This method
284
+ * merge multiple event types into one for notification.
285
+ * @param eventTypes - Context event types
286
+ */
287
+ private setupNotification(...eventTypes: ContextEventType[]) {
288
+ for (const type of eventTypes) {
289
+ this.context.on(type, ({binding, context}) => {
290
+ // No need to schedule notifications if no observers are present
291
+ if (!this._observers || this._observers.size === 0) return;
292
+ // Track pending events
293
+ this.pendingNotifications++;
294
+ // Take a snapshot of current observers to ensure notifications of this
295
+ // event will only be sent to current ones. Emit a new event to notify
296
+ // current context observers.
297
+ this.emitEvent('notification', {
298
+ type,
299
+ binding,
300
+ context,
301
+ observers: new Set(this._observers),
302
+ });
303
+ });
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Wait until observers are notified for all of currently pending notification
309
+ * events.
310
+ *
311
+ * This method is for test only to perform assertions after observers are
312
+ * notified for relevant events.
313
+ */
314
+ async waitUntilPendingNotificationsDone(timeout?: number) {
315
+ const count = this.pendingNotifications;
316
+ if (count === 0) return;
317
+ await multiple(this, 'observersNotified', {count, timeout});
318
+ }
319
+
320
+ /**
321
+ * Add a context event observer to the context
322
+ * @param observer - Context observer instance or function
323
+ */
324
+ subscribe(observer: ContextEventObserver): Subscription {
325
+ this._observers = this._observers ?? new Set();
326
+ this.setupEventHandlersIfNeeded();
327
+ this._observers.add(observer);
328
+ return new ContextSubscription(this.context, observer);
329
+ }
330
+
331
+ /**
332
+ * Remove the context event observer from the context
333
+ * @param observer - Context event observer
334
+ */
335
+ unsubscribe(observer: ContextEventObserver): boolean {
336
+ if (!this._observers) return false;
337
+ return this._observers.delete(observer);
338
+ }
339
+
340
+ /**
341
+ * Check if an observer is subscribed to this context
342
+ * @param observer - Context observer
343
+ */
344
+ isSubscribed(observer: ContextObserver) {
345
+ if (!this._observers) return false;
346
+ return this._observers.has(observer);
347
+ }
348
+
349
+ /**
350
+ * Handle errors caught during the notification of observers
351
+ * @param err - Error
352
+ */
353
+ private handleNotificationError(err: unknown) {
354
+ // Bubbling up the error event over the context chain
355
+ // until we find an error listener
356
+ let ctx: Context | undefined = this.context;
357
+ while (ctx) {
358
+ if (ctx.listenerCount('error') === 0) {
359
+ // No error listener found, try its parent
360
+ ctx = ctx.parent;
361
+ continue;
362
+ }
363
+ this._debug('Emitting error to context %s', ctx.name, err);
364
+ ctx.emitError(err);
365
+ return;
366
+ }
367
+ // No context with error listeners found
368
+ this._debug('No error handler is configured for the context chain', err);
369
+ // Let it crash now by emitting an error event
370
+ this.context.emitError(err);
371
+ }
372
+
373
+ /**
374
+ * Close the context: clear observers, stop notifications, and remove event
375
+ * listeners from its parent context.
376
+ *
377
+ * @remarks
378
+ * This method MUST be called to avoid memory leaks once a context object is
379
+ * no longer needed and should be recycled. An example is the `RequestContext`,
380
+ * which is created per request.
381
+ */
382
+ close() {
383
+ this._observers = undefined;
384
+ if (this.notificationQueue != null) {
385
+ // Cancel the notification iterator
386
+ this.notificationQueue.return!(undefined).catch(err => {
387
+ this.handleNotificationError(err);
388
+ });
389
+ this.notificationQueue = undefined;
390
+ }
391
+ if (this.context.parent && this._parentContextEventListener) {
392
+ this.context.parent.removeListener(
393
+ 'bind',
394
+ this._parentContextEventListener,
395
+ );
396
+ this.context.parent.removeListener(
397
+ 'unbind',
398
+ this._parentContextEventListener,
399
+ );
400
+ this._parentContextEventListener = undefined;
401
+ }
402
+ }
403
+ }