@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.
- package/CHANGELOG.md +80 -0
- package/dist/binding-filter.d.ts +20 -2
- package/dist/binding-filter.js +58 -9
- package/dist/binding-filter.js.map +1 -1
- package/dist/binding.d.ts +50 -2
- package/dist/binding.js +33 -1
- package/dist/binding.js.map +1 -1
- package/dist/context-event.d.ts +23 -0
- package/dist/context-event.js +7 -0
- package/dist/context-event.js.map +1 -0
- package/dist/context-observer.d.ts +1 -36
- package/dist/context-subscription.d.ts +147 -0
- package/dist/context-subscription.js +336 -0
- package/dist/context-subscription.js.map +1 -0
- package/dist/context-tag-indexer.d.ts +42 -0
- package/dist/context-tag-indexer.js +134 -0
- package/dist/context-tag-indexer.js.map +1 -0
- package/dist/context-view.d.ts +2 -1
- package/dist/context-view.js.map +1 -1
- package/dist/context.d.ts +44 -68
- package/dist/context.js +69 -249
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +9 -2
- package/dist/inject.js +60 -0
- package/dist/inject.js.map +1 -1
- package/dist/interceptor.js +4 -4
- package/dist/interceptor.js.map +1 -1
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +1 -5
- package/dist/invocation.js.map +1 -1
- package/dist/json-types.d.ts +28 -0
- package/dist/json-types.js +7 -0
- package/dist/json-types.js.map +1 -0
- package/dist/resolution-session.d.ts +2 -6
- package/dist/resolution-session.js +0 -4
- package/dist/resolution-session.js.map +1 -1
- package/dist/value-promise.d.ts +1 -3
- package/package.json +9 -9
- package/src/binding-filter.ts +83 -11
- package/src/binding.ts +79 -3
- package/src/context-event.ts +30 -0
- package/src/context-observer.ts +1 -38
- package/src/context-subscription.ts +403 -0
- package/src/context-tag-indexer.ts +149 -0
- package/src/context-view.ts +2 -5
- package/src/context.ts +104 -290
- package/src/index.ts +3 -0
- package/src/inject.ts +65 -0
- package/src/interceptor.ts +7 -6
- package/src/invocation.ts +1 -3
- package/src/json-types.ts +35 -0
- package/src/resolution-session.ts +4 -7
- 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():
|
|
595
|
-
const json:
|
|
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;
|
package/src/context-observer.ts
CHANGED
|
@@ -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
|
+
}
|