@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/context.ts
CHANGED
|
@@ -6,22 +6,25 @@
|
|
|
6
6
|
import debugFactory from 'debug';
|
|
7
7
|
import {EventEmitter} from 'events';
|
|
8
8
|
import {v1 as uuidv1} from 'uuid';
|
|
9
|
-
import {Binding, BindingTag} from './binding';
|
|
9
|
+
import {Binding, BindingInspectOptions, BindingTag} from './binding';
|
|
10
10
|
import {
|
|
11
11
|
ConfigurationResolver,
|
|
12
12
|
DefaultConfigurationResolver,
|
|
13
13
|
} from './binding-config';
|
|
14
|
-
import {
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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';
|
|
27
|
+
import {JSONObject} from './json-types';
|
|
25
28
|
import {ContextBindings} from './keys';
|
|
26
29
|
import {
|
|
27
30
|
asResolutionOptions,
|
|
@@ -36,21 +39,6 @@ import {
|
|
|
36
39
|
ValueOrPromise,
|
|
37
40
|
} from './value-promise';
|
|
38
41
|
|
|
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
42
|
const debug = debugFactory('loopback:context');
|
|
55
43
|
|
|
56
44
|
/**
|
|
@@ -68,41 +56,24 @@ export class Context extends EventEmitter {
|
|
|
68
56
|
protected readonly registry: Map<string, Binding> = new Map();
|
|
69
57
|
|
|
70
58
|
/**
|
|
71
|
-
*
|
|
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.
|
|
59
|
+
* Indexer for bindings by tag
|
|
81
60
|
*/
|
|
82
|
-
protected
|
|
83
|
-
| Map<
|
|
84
|
-
string,
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
-
(...args: any[]) => void
|
|
87
|
-
>
|
|
88
|
-
| undefined;
|
|
61
|
+
protected readonly tagIndexer: ContextTagIndexer;
|
|
89
62
|
|
|
90
63
|
/**
|
|
91
|
-
*
|
|
92
|
-
* first observer is added.
|
|
64
|
+
* Manager for observer subscriptions
|
|
93
65
|
*/
|
|
94
|
-
|
|
66
|
+
readonly subscriptionManager: ContextSubscriptionManager;
|
|
95
67
|
|
|
96
68
|
/**
|
|
97
|
-
*
|
|
98
|
-
* processed by observers.
|
|
69
|
+
* Parent context
|
|
99
70
|
*/
|
|
100
|
-
|
|
71
|
+
protected _parent?: Context;
|
|
101
72
|
|
|
102
73
|
/**
|
|
103
|
-
*
|
|
74
|
+
* Configuration resolver
|
|
104
75
|
*/
|
|
105
|
-
|
|
76
|
+
protected configResolver: ConfigurationResolver;
|
|
106
77
|
|
|
107
78
|
/**
|
|
108
79
|
* Create a new context.
|
|
@@ -128,12 +99,34 @@ export class Context extends EventEmitter {
|
|
|
128
99
|
*/
|
|
129
100
|
constructor(_parent?: Context | string, name?: string) {
|
|
130
101
|
super();
|
|
102
|
+
// The number of listeners can grow with the number of child contexts
|
|
103
|
+
// For example, each request can add a listener to the RestServer and the
|
|
104
|
+
// listener is removed when the request processing is finished.
|
|
105
|
+
// See https://github.com/strongloop/loopback-next/issues/4363
|
|
106
|
+
this.setMaxListeners(Infinity);
|
|
131
107
|
if (typeof _parent === 'string') {
|
|
132
108
|
name = _parent;
|
|
133
109
|
_parent = undefined;
|
|
134
110
|
}
|
|
135
111
|
this._parent = _parent;
|
|
136
|
-
this.name = name ??
|
|
112
|
+
this.name = name ?? this.generateName();
|
|
113
|
+
this.tagIndexer = new ContextTagIndexer(this);
|
|
114
|
+
this.subscriptionManager = new ContextSubscriptionManager(this);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private generateName() {
|
|
118
|
+
const id = uuidv1();
|
|
119
|
+
let prefix = `${this.constructor.name}-`;
|
|
120
|
+
if (prefix === 'Context-') prefix = '';
|
|
121
|
+
return `${prefix}${id}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @internal
|
|
126
|
+
* Getter for ContextSubscriptionManager
|
|
127
|
+
*/
|
|
128
|
+
get parent() {
|
|
129
|
+
return this._parent;
|
|
137
130
|
}
|
|
138
131
|
|
|
139
132
|
/**
|
|
@@ -141,8 +134,7 @@ export class Context extends EventEmitter {
|
|
|
141
134
|
* as the prefix
|
|
142
135
|
* @param args - Arguments for the debug
|
|
143
136
|
*/
|
|
144
|
-
|
|
145
|
-
private _debug(...args: any[]) {
|
|
137
|
+
private _debug(...args: unknown[]) {
|
|
146
138
|
/* istanbul ignore if */
|
|
147
139
|
if (!debug.enabled) return;
|
|
148
140
|
const formatter = args.shift();
|
|
@@ -154,176 +146,22 @@ export class Context extends EventEmitter {
|
|
|
154
146
|
}
|
|
155
147
|
|
|
156
148
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*/
|
|
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
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
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
|
|
149
|
+
* A strongly-typed method to emit context events
|
|
150
|
+
* @param type Event type
|
|
151
|
+
* @param event Context event
|
|
184
152
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
153
|
+
emitEvent<T extends ContextEvent>(type: string, event: T) {
|
|
154
|
+
this.emit(type, event);
|
|
221
155
|
}
|
|
222
156
|
|
|
223
157
|
/**
|
|
224
|
-
*
|
|
225
|
-
* @param err
|
|
158
|
+
* Emit an `error` event
|
|
159
|
+
* @param err Error
|
|
226
160
|
*/
|
|
227
|
-
|
|
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
|
|
161
|
+
emitError(err: unknown) {
|
|
245
162
|
this.emit('error', err);
|
|
246
163
|
}
|
|
247
164
|
|
|
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
165
|
/**
|
|
328
166
|
* Create a binding with the given key in the context. If a locked binding
|
|
329
167
|
* already exists with the same key, an error will be thrown.
|
|
@@ -357,9 +195,13 @@ export class Context extends EventEmitter {
|
|
|
357
195
|
this.registry.set(key, binding);
|
|
358
196
|
if (existingBinding !== binding) {
|
|
359
197
|
if (existingBinding != null) {
|
|
360
|
-
this.
|
|
198
|
+
this.emitEvent('unbind', {
|
|
199
|
+
binding: existingBinding,
|
|
200
|
+
context: this,
|
|
201
|
+
type: 'unbind',
|
|
202
|
+
});
|
|
361
203
|
}
|
|
362
|
-
this.
|
|
204
|
+
this.emitEvent('bind', {binding, context: this, type: 'bind'});
|
|
363
205
|
}
|
|
364
206
|
return this;
|
|
365
207
|
}
|
|
@@ -505,7 +347,7 @@ export class Context extends EventEmitter {
|
|
|
505
347
|
if (binding?.isLocked)
|
|
506
348
|
throw new Error(`Cannot unbind key "${key}" of a locked binding`);
|
|
507
349
|
this.registry.delete(key);
|
|
508
|
-
this.
|
|
350
|
+
this.emitEvent('unbind', {binding, context: this, type: 'unbind'});
|
|
509
351
|
return true;
|
|
510
352
|
}
|
|
511
353
|
|
|
@@ -514,10 +356,7 @@ export class Context extends EventEmitter {
|
|
|
514
356
|
* @param observer - Context observer instance or function
|
|
515
357
|
*/
|
|
516
358
|
subscribe(observer: ContextEventObserver): Subscription {
|
|
517
|
-
|
|
518
|
-
this.setupEventHandlersIfNeeded();
|
|
519
|
-
this.observers.add(observer);
|
|
520
|
-
return new ContextSubscription(this, observer);
|
|
359
|
+
return this.subscriptionManager.subscribe(observer);
|
|
521
360
|
}
|
|
522
361
|
|
|
523
362
|
/**
|
|
@@ -525,8 +364,7 @@ export class Context extends EventEmitter {
|
|
|
525
364
|
* @param observer - Context event observer
|
|
526
365
|
*/
|
|
527
366
|
unsubscribe(observer: ContextEventObserver): boolean {
|
|
528
|
-
|
|
529
|
-
return this.observers.delete(observer);
|
|
367
|
+
return this.subscriptionManager.unsubscribe(observer);
|
|
530
368
|
}
|
|
531
369
|
|
|
532
370
|
/**
|
|
@@ -540,20 +378,8 @@ export class Context extends EventEmitter {
|
|
|
540
378
|
*/
|
|
541
379
|
close() {
|
|
542
380
|
this._debug('Closing context...');
|
|
543
|
-
this.
|
|
544
|
-
|
|
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
|
-
}
|
|
381
|
+
this.subscriptionManager.close();
|
|
382
|
+
this.tagIndexer.close();
|
|
557
383
|
}
|
|
558
384
|
|
|
559
385
|
/**
|
|
@@ -561,8 +387,7 @@ export class Context extends EventEmitter {
|
|
|
561
387
|
* @param observer - Context observer
|
|
562
388
|
*/
|
|
563
389
|
isSubscribed(observer: ContextObserver) {
|
|
564
|
-
|
|
565
|
-
return this.observers.has(observer);
|
|
390
|
+
return this.subscriptionManager.isSubscribed(observer);
|
|
566
391
|
}
|
|
567
392
|
|
|
568
393
|
/**
|
|
@@ -579,34 +404,6 @@ export class Context extends EventEmitter {
|
|
|
579
404
|
return view;
|
|
580
405
|
}
|
|
581
406
|
|
|
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
407
|
/**
|
|
611
408
|
* Check if a binding exists with the given key in the local context without
|
|
612
409
|
* delegating to the parent context
|
|
@@ -658,6 +455,11 @@ export class Context extends EventEmitter {
|
|
|
658
455
|
find<ValueType = BoundValue>(
|
|
659
456
|
pattern?: string | RegExp | BindingFilter,
|
|
660
457
|
): Readonly<Binding<ValueType>>[] {
|
|
458
|
+
// Optimize if the binding filter is for tags
|
|
459
|
+
if (typeof pattern === 'function' && isBindingTagFilter(pattern)) {
|
|
460
|
+
return this._findByTagIndex(pattern.bindingTagPattern);
|
|
461
|
+
}
|
|
462
|
+
|
|
661
463
|
const bindings: Readonly<Binding<ValueType>>[] = [];
|
|
662
464
|
const filter = filterByKey(pattern);
|
|
663
465
|
|
|
@@ -689,6 +491,18 @@ export class Context extends EventEmitter {
|
|
|
689
491
|
return this.find(filterByTag(tagFilter));
|
|
690
492
|
}
|
|
691
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Find bindings by tag leveraging indexes
|
|
496
|
+
* @param tag - Tag name pattern or name/value pairs
|
|
497
|
+
*/
|
|
498
|
+
protected _findByTagIndex<ValueType = BoundValue>(
|
|
499
|
+
tag: BindingTag | RegExp,
|
|
500
|
+
): Readonly<Binding<ValueType>>[] {
|
|
501
|
+
const currentBindings = this.tagIndexer.findByTagIndex(tag);
|
|
502
|
+
const parentBindings = this._parent && this._parent?._findByTagIndex(tag);
|
|
503
|
+
return this._mergeWithParent(currentBindings, parentBindings);
|
|
504
|
+
}
|
|
505
|
+
|
|
692
506
|
protected _mergeWithParent<ValueType>(
|
|
693
507
|
childList: Readonly<Binding<ValueType>>[],
|
|
694
508
|
parentList?: Readonly<Binding<ValueType>>[],
|
|
@@ -968,8 +782,8 @@ export class Context extends EventEmitter {
|
|
|
968
782
|
/**
|
|
969
783
|
* Create a plain JSON object for the context
|
|
970
784
|
*/
|
|
971
|
-
toJSON():
|
|
972
|
-
const bindings:
|
|
785
|
+
toJSON(): JSONObject {
|
|
786
|
+
const bindings: JSONObject = {};
|
|
973
787
|
for (const [k, v] of this.registry) {
|
|
974
788
|
bindings[k] = v.toJSON();
|
|
975
789
|
}
|
|
@@ -979,39 +793,39 @@ export class Context extends EventEmitter {
|
|
|
979
793
|
/**
|
|
980
794
|
* Inspect the context and dump out a JSON object representing the context
|
|
981
795
|
* hierarchy
|
|
796
|
+
* @param options - Options for inspect
|
|
982
797
|
*/
|
|
983
798
|
// TODO(rfeng): Evaluate https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects
|
|
984
|
-
inspect():
|
|
985
|
-
|
|
799
|
+
inspect(options: ContextInspectOptions = {}): JSONObject {
|
|
800
|
+
options = {
|
|
801
|
+
includeParent: true,
|
|
802
|
+
includeInjections: false,
|
|
803
|
+
...options,
|
|
804
|
+
};
|
|
805
|
+
const bindings: JSONObject = {};
|
|
806
|
+
for (const [k, v] of this.registry) {
|
|
807
|
+
bindings[k] = v.inspect(options);
|
|
808
|
+
}
|
|
809
|
+
const json: JSONObject = {
|
|
986
810
|
name: this.name,
|
|
987
|
-
bindings
|
|
811
|
+
bindings,
|
|
988
812
|
};
|
|
813
|
+
if (!options.includeParent) return json;
|
|
989
814
|
if (this._parent) {
|
|
990
|
-
json.parent = this._parent.inspect();
|
|
815
|
+
json.parent = this._parent.inspect(options);
|
|
991
816
|
}
|
|
992
817
|
return json;
|
|
993
818
|
}
|
|
994
819
|
}
|
|
995
820
|
|
|
996
821
|
/**
|
|
997
|
-
*
|
|
822
|
+
* Options for context.inspect()
|
|
998
823
|
*/
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
}
|
|
824
|
+
export interface ContextInspectOptions extends BindingInspectOptions {
|
|
825
|
+
/**
|
|
826
|
+
* The flag to control if parent context should be inspected
|
|
827
|
+
*/
|
|
828
|
+
includeParent?: boolean;
|
|
1015
829
|
}
|
|
1016
830
|
|
|
1017
831
|
/**
|
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';
|
|
@@ -25,3 +27,4 @@ export * from './provider';
|
|
|
25
27
|
export * from './resolution-session';
|
|
26
28
|
export * from './resolver';
|
|
27
29
|
export * from './value-promise';
|
|
30
|
+
export * from './json-types';
|
package/src/inject.ts
CHANGED
|
@@ -18,11 +18,13 @@ import {
|
|
|
18
18
|
BindingSelector,
|
|
19
19
|
filterByTag,
|
|
20
20
|
isBindingAddress,
|
|
21
|
+
isBindingTagFilter,
|
|
21
22
|
} from './binding-filter';
|
|
22
23
|
import {BindingAddress} from './binding-key';
|
|
23
24
|
import {BindingComparator} from './binding-sorter';
|
|
24
25
|
import {BindingCreationPolicy, Context} from './context';
|
|
25
26
|
import {ContextView, createViewGetter} from './context-view';
|
|
27
|
+
import {JSONObject} from './json-types';
|
|
26
28
|
import {ResolutionOptions, ResolutionSession} from './resolution-session';
|
|
27
29
|
import {BoundValue, ValueOrPromise} from './value-promise';
|
|
28
30
|
|
|
@@ -698,3 +700,66 @@ export function describeInjectedProperties(
|
|
|
698
700
|
) ?? {};
|
|
699
701
|
return metadata;
|
|
700
702
|
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Inspect injections for a binding created with `toClass` or `toProvider`
|
|
706
|
+
* @param binding - Binding object
|
|
707
|
+
*/
|
|
708
|
+
export function inspectInjections(binding: Readonly<Binding<unknown>>) {
|
|
709
|
+
const json: JSONObject = {};
|
|
710
|
+
const ctor = binding.valueConstructor ?? binding.providerConstructor;
|
|
711
|
+
if (ctor == null) return json;
|
|
712
|
+
const constructorInjections = describeInjectedArguments(ctor, '').map(
|
|
713
|
+
inspectInjection,
|
|
714
|
+
);
|
|
715
|
+
if (constructorInjections.length) {
|
|
716
|
+
json.constructorArguments = constructorInjections;
|
|
717
|
+
}
|
|
718
|
+
const propertyInjections = describeInjectedProperties(ctor.prototype);
|
|
719
|
+
const properties: JSONObject = {};
|
|
720
|
+
for (const p in propertyInjections) {
|
|
721
|
+
properties[p] = inspectInjection(propertyInjections[p]);
|
|
722
|
+
}
|
|
723
|
+
if (Object.keys(properties).length) {
|
|
724
|
+
json.properties = properties;
|
|
725
|
+
}
|
|
726
|
+
return json;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Inspect an injection
|
|
731
|
+
* @param injection - Injection information
|
|
732
|
+
*/
|
|
733
|
+
function inspectInjection(injection: Readonly<Injection<unknown>>) {
|
|
734
|
+
const injectionInfo = ResolutionSession.describeInjection(injection);
|
|
735
|
+
const descriptor: JSONObject = {};
|
|
736
|
+
if (injectionInfo.targetName) {
|
|
737
|
+
descriptor.targetName = injectionInfo.targetName;
|
|
738
|
+
}
|
|
739
|
+
if (isBindingAddress(injectionInfo.bindingSelector)) {
|
|
740
|
+
// Binding key
|
|
741
|
+
descriptor.bindingKey = injectionInfo.bindingSelector.toString();
|
|
742
|
+
} else if (isBindingTagFilter(injectionInfo.bindingSelector)) {
|
|
743
|
+
// Binding tag filter
|
|
744
|
+
descriptor.bindingTagPattern = JSON.parse(
|
|
745
|
+
JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern),
|
|
746
|
+
);
|
|
747
|
+
} else {
|
|
748
|
+
// Binding filter function
|
|
749
|
+
descriptor.bindingFilter =
|
|
750
|
+
injectionInfo.bindingSelector?.name ?? '<function>';
|
|
751
|
+
}
|
|
752
|
+
// Inspect metadata
|
|
753
|
+
if (injectionInfo.metadata) {
|
|
754
|
+
if (
|
|
755
|
+
injectionInfo.metadata.decorator &&
|
|
756
|
+
injectionInfo.metadata.decorator !== '@inject'
|
|
757
|
+
) {
|
|
758
|
+
descriptor.decorator = injectionInfo.metadata.decorator;
|
|
759
|
+
}
|
|
760
|
+
if (injectionInfo.metadata.optional) {
|
|
761
|
+
descriptor.optional = injectionInfo.metadata.optional;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return descriptor;
|
|
765
|
+
}
|
package/src/interceptor.ts
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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,
|