1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import debugFactory from 'debug';
|
7 | import {EventEmitter} from 'events';
|
8 | import {promisify} from 'util';
|
9 | import {Binding} from './binding';
|
10 | import {BindingFilter} from './binding-filter';
|
11 | import {BindingComparator} from './binding-sorter';
|
12 | import {Context} from './context';
|
13 | import {ContextEvent} from './context-event';
|
14 | import {ContextEventType, ContextObserver} from './context-observer';
|
15 | import {Subscription} from './context-subscription';
|
16 | import {Getter} from './inject';
|
17 | import {
|
18 | asResolutionOptions,
|
19 | ResolutionOptions,
|
20 | ResolutionOptionsOrSession,
|
21 | ResolutionSession,
|
22 | } from './resolution-session';
|
23 | import {isPromiseLike, resolveList, ValueOrPromise} from './value-promise';
|
24 | const debug = debugFactory('loopback:context:view');
|
25 | const nextTick = promisify(process.nextTick);
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | export interface ContextViewEvent<T> extends ContextEvent {
|
31 | |
32 |
|
33 |
|
34 | cachedValue?: T;
|
35 | }
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | export class ContextView<T = unknown>
|
54 | extends EventEmitter
|
55 | implements ContextObserver
|
56 | {
|
57 | |
58 |
|
59 |
|
60 | protected _cachedBindings: Readonly<Binding<T>>[] | undefined;
|
61 | |
62 |
|
63 |
|
64 | protected _cachedValues: Map<Readonly<Binding<T>>, T> | undefined;
|
65 | private _subscription: Subscription | undefined;
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | constructor(
|
74 | public readonly context: Context,
|
75 | public readonly filter: BindingFilter,
|
76 | public readonly comparator?: BindingComparator,
|
77 | private resolutionOptions?: Omit<ResolutionOptions, 'session'>,
|
78 | ) {
|
79 | super();
|
80 | }
|
81 |
|
82 | |
83 |
|
84 |
|
85 |
|
86 | private updateCachedValues(values: T[]) {
|
87 | if (this._cachedBindings == null) return undefined;
|
88 | this._cachedValues = new Map();
|
89 | for (let i = 0; i < this._cachedBindings?.length; i++) {
|
90 | this._cachedValues.set(this._cachedBindings[i], values[i]);
|
91 | }
|
92 | return this._cachedValues;
|
93 | }
|
94 |
|
95 | |
96 |
|
97 |
|
98 | private getCachedValues() {
|
99 | return Array.from(this._cachedValues?.values() ?? []);
|
100 | }
|
101 |
|
102 | |
103 |
|
104 |
|
105 | open() {
|
106 | debug('Start listening on changes of context %s', this.context.name);
|
107 | if (this.context.isSubscribed(this)) {
|
108 | return this._subscription;
|
109 | }
|
110 | this._subscription = this.context.subscribe(this);
|
111 | return this._subscription;
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 | close() {
|
118 | debug('Stop listening on changes of context %s', this.context.name);
|
119 | if (!this._subscription || this._subscription.closed) return;
|
120 | this._subscription.unsubscribe();
|
121 | this._subscription = undefined;
|
122 | this.emit('close');
|
123 | }
|
124 |
|
125 | |
126 |
|
127 |
|
128 |
|
129 | get bindings(): Readonly<Binding<T>>[] {
|
130 | debug('Reading bindings');
|
131 | if (this._cachedBindings == null) {
|
132 | this._cachedBindings = this.findBindings();
|
133 | }
|
134 | return this._cachedBindings;
|
135 | }
|
136 |
|
137 | |
138 |
|
139 |
|
140 | protected findBindings(): Readonly<Binding<T>>[] {
|
141 | debug('Finding matching bindings');
|
142 | const found = this.context.find(this.filter);
|
143 | if (typeof this.comparator === 'function') {
|
144 | found.sort(this.comparator);
|
145 | }
|
146 |
|
147 | if (debug.enabled) {
|
148 | debug(
|
149 | 'Bindings found',
|
150 | found.map(b => b.key),
|
151 | );
|
152 | }
|
153 | return found;
|
154 | }
|
155 |
|
156 | |
157 |
|
158 |
|
159 | observe(
|
160 | event: ContextEventType,
|
161 | binding: Readonly<Binding<unknown>>,
|
162 | context: Context,
|
163 | ) {
|
164 | const ctxEvent: ContextViewEvent<T> = {
|
165 | context,
|
166 | binding,
|
167 | type: event,
|
168 | };
|
169 | debug('Observed event %s %s %s', event, binding.key, context.name);
|
170 |
|
171 | if (event === 'unbind') {
|
172 | const cachedValue = this._cachedValues?.get(
|
173 | binding as Readonly<Binding<T>>,
|
174 | );
|
175 | this.emit(event, {...ctxEvent, cachedValue});
|
176 | } else {
|
177 | this.emit(event, ctxEvent);
|
178 | }
|
179 |
|
180 | this.refresh();
|
181 | }
|
182 |
|
183 | |
184 |
|
185 |
|
186 | refresh() {
|
187 | debug('Refreshing the view by invalidating cache');
|
188 | this._cachedBindings = undefined;
|
189 | this._cachedValues = undefined;
|
190 | this.emit('refresh');
|
191 | }
|
192 |
|
193 | |
194 |
|
195 |
|
196 |
|
197 | resolve(session?: ResolutionOptionsOrSession): ValueOrPromise<T[]> {
|
198 | debug('Resolving values');
|
199 | if (this._cachedValues != null) {
|
200 | return this.getCachedValues();
|
201 | }
|
202 | const bindings = this.bindings;
|
203 | let result = resolveList(bindings, b => {
|
204 | const options = {
|
205 | ...this.resolutionOptions,
|
206 | ...asResolutionOptions(session),
|
207 | };
|
208 |
|
209 |
|
210 |
|
211 | options.session = undefined;
|
212 | return b.getValue(this.context, options);
|
213 | });
|
214 | if (isPromiseLike(result)) {
|
215 | result = result.then(values => {
|
216 | const list = values.filter(v => v != null) as T[];
|
217 | this.updateCachedValues(list);
|
218 | this.emit('resolve', list);
|
219 | return list;
|
220 | });
|
221 | } else {
|
222 |
|
223 | const list = (result = result.filter(v => v != null) as T[]);
|
224 | this.updateCachedValues(list);
|
225 | this.emit('resolve', list);
|
226 | }
|
227 | return result as ValueOrPromise<T[]>;
|
228 | }
|
229 |
|
230 | |
231 |
|
232 |
|
233 |
|
234 | async values(session?: ResolutionOptionsOrSession): Promise<T[]> {
|
235 | debug('Reading values');
|
236 |
|
237 | await nextTick();
|
238 | if (this._cachedValues == null) {
|
239 | return this.resolve(session);
|
240 | }
|
241 | return this.getCachedValues();
|
242 | }
|
243 |
|
244 | |
245 |
|
246 |
|
247 | asGetter(session?: ResolutionOptionsOrSession): Getter<T[]> {
|
248 | return () => this.values(session);
|
249 | }
|
250 |
|
251 | |
252 |
|
253 |
|
254 | async singleValue(
|
255 | session?: ResolutionOptionsOrSession,
|
256 | ): Promise<T | undefined> {
|
257 | const values = await this.values(session);
|
258 | if (values.length === 0) return undefined;
|
259 | if (values.length === 1) return values[0];
|
260 | throw new Error(
|
261 | 'The ContextView has more than one value. Use values() to access them.',
|
262 | );
|
263 | }
|
264 |
|
265 | |
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 | on(
|
272 | eventName: 'bind',
|
273 | listener: <V>(event: ContextViewEvent<V>) => void,
|
274 | ): this;
|
275 |
|
276 | |
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 | on(
|
283 | eventName: 'unbind',
|
284 | listener: <V>(event: ContextViewEvent<V> & {cachedValue?: V}) => void,
|
285 | ): this;
|
286 |
|
287 | |
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | on(eventName: 'refresh', listener: () => void): this;
|
295 |
|
296 | |
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 | on(eventName: 'refresh', listener: <V>(result: V[]) => void): this;
|
305 |
|
306 | |
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | on(eventName: 'close', listener: () => void): this;
|
315 |
|
316 |
|
317 |
|
318 | on(event: string | symbol, listener: (...args: any[]) => void): this;
|
319 |
|
320 |
|
321 | on(event: string | symbol, listener: (...args: any[]) => void): this {
|
322 | return super.on(event, listener);
|
323 | }
|
324 |
|
325 | |
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 | once(
|
332 | eventName: 'bind',
|
333 | listener: <V>(event: ContextViewEvent<V>) => void,
|
334 | ): this;
|
335 |
|
336 | |
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | once(
|
343 | eventName: 'unbind',
|
344 | listener: <V>(event: ContextViewEvent<V> & {cachedValue?: V}) => void,
|
345 | ): this;
|
346 |
|
347 | |
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 | once(eventName: 'refresh', listener: () => void): this;
|
355 |
|
356 | |
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 | once(eventName: 'refresh', listener: <V>(result: V[]) => void): this;
|
365 |
|
366 | |
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | once(eventName: 'close', listener: () => void): this;
|
375 |
|
376 |
|
377 |
|
378 | once(event: string | symbol, listener: (...args: any[]) => void): this;
|
379 |
|
380 |
|
381 | once(event: string | symbol, listener: (...args: any[]) => void): this {
|
382 | return super.once(event, listener);
|
383 | }
|
384 | }
|
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 | export function createViewGetter<T = unknown>(
|
393 | ctx: Context,
|
394 | bindingFilter: BindingFilter,
|
395 | session?: ResolutionSession,
|
396 | ): Getter<T[]>;
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 | export function createViewGetter<T = unknown>(
|
407 | ctx: Context,
|
408 | bindingFilter: BindingFilter,
|
409 | bindingComparator?: BindingComparator,
|
410 | session?: ResolutionOptionsOrSession,
|
411 | ): Getter<T[]>;
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 | export function createViewGetter<T = unknown>(
|
422 | ctx: Context,
|
423 | bindingFilter: BindingFilter,
|
424 | bindingComparatorOrSession?: BindingComparator | ResolutionSession,
|
425 | session?: ResolutionOptionsOrSession,
|
426 | ): Getter<T[]> {
|
427 | let bindingComparator: BindingComparator | undefined = undefined;
|
428 | if (typeof bindingComparatorOrSession === 'function') {
|
429 | bindingComparator = bindingComparatorOrSession;
|
430 | } else if (bindingComparatorOrSession instanceof ResolutionSession) {
|
431 | session = bindingComparatorOrSession;
|
432 | }
|
433 |
|
434 | const options = asResolutionOptions(session);
|
435 | const view = new ContextView<T>(
|
436 | ctx,
|
437 | bindingFilter,
|
438 | bindingComparator,
|
439 | options,
|
440 | );
|
441 | view.open();
|
442 | return view.asGetter(options);
|
443 | }
|