UNPKG

8.86 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
3// Node module: @loopback/context
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.createViewGetter = exports.ContextView = void 0;
8const tslib_1 = require("tslib");
9const events_1 = require("events");
10const debug_1 = tslib_1.__importDefault(require("debug"));
11const resolution_session_1 = require("./resolution-session");
12const value_promise_1 = require("./value-promise");
13const debug = (0, debug_1.default)('loopback:context:view');
14/**
15 * `ContextView` provides a view for a given context chain to maintain a live
16 * list of matching bindings and their resolved values within the context
17 * hierarchy.
18 *
19 * This class is the key utility to implement dynamic extensions for extension
20 * points. For example, the RestServer can react to `controller` bindings even
21 * they are added/removed/updated after the application starts.
22 *
23 * `ContextView` is an event emitter that emits the following events:
24 * - 'bind': when a binding is added to the view
25 * - 'unbind': when a binding is removed from the view
26 * - 'close': when the view is closed (stopped observing context events)
27 * - 'refresh': when the view is refreshed as bindings are added/removed
28 * - 'resolve': when the cached values are resolved and updated
29 */
30class ContextView extends events_1.EventEmitter {
31 /**
32 * Create a context view
33 * @param context - Context object to watch
34 * @param filter - Binding filter to match bindings of interest
35 * @param comparator - Comparator to sort the matched bindings
36 */
37 constructor(context, filter, comparator, resolutionOptions) {
38 super();
39 this.context = context;
40 this.filter = filter;
41 this.comparator = comparator;
42 this.resolutionOptions = resolutionOptions;
43 }
44 /**
45 * Update the cached values keyed by binding
46 * @param values - An array of resolved values
47 */
48 updateCachedValues(values) {
49 var _a;
50 if (this._cachedBindings == null)
51 return undefined;
52 this._cachedValues = new Map();
53 for (let i = 0; i < ((_a = this._cachedBindings) === null || _a === void 0 ? void 0 : _a.length); i++) {
54 this._cachedValues.set(this._cachedBindings[i], values[i]);
55 }
56 return this._cachedValues;
57 }
58 /**
59 * Get an array of cached values
60 */
61 getCachedValues() {
62 var _a, _b;
63 return Array.from((_b = (_a = this._cachedValues) === null || _a === void 0 ? void 0 : _a.values()) !== null && _b !== void 0 ? _b : []);
64 }
65 /**
66 * Start listening events from the context
67 */
68 open() {
69 debug('Start listening on changes of context %s', this.context.name);
70 if (this.context.isSubscribed(this)) {
71 return this._subscription;
72 }
73 this._subscription = this.context.subscribe(this);
74 return this._subscription;
75 }
76 /**
77 * Stop listening events from the context
78 */
79 close() {
80 debug('Stop listening on changes of context %s', this.context.name);
81 if (!this._subscription || this._subscription.closed)
82 return;
83 this._subscription.unsubscribe();
84 this._subscription = undefined;
85 this.emit('close');
86 }
87 /**
88 * Get the list of matched bindings. If they are not cached, it tries to find
89 * them from the context.
90 */
91 get bindings() {
92 debug('Reading bindings');
93 if (this._cachedBindings == null) {
94 this._cachedBindings = this.findBindings();
95 }
96 return this._cachedBindings;
97 }
98 /**
99 * Find matching bindings and refresh the cache
100 */
101 findBindings() {
102 debug('Finding matching bindings');
103 const found = this.context.find(this.filter);
104 if (typeof this.comparator === 'function') {
105 found.sort(this.comparator);
106 }
107 /* istanbul ignore if */
108 if (debug.enabled) {
109 debug('Bindings found', found.map(b => b.key));
110 }
111 return found;
112 }
113 /**
114 * Listen on `bind` or `unbind` and invalidate the cache
115 */
116 observe(event, binding, context) {
117 var _a;
118 const ctxEvent = {
119 context,
120 binding,
121 type: event,
122 };
123 debug('Observed event %s %s %s', event, binding.key, context.name);
124 if (event === 'unbind') {
125 const cachedValue = (_a = this._cachedValues) === null || _a === void 0 ? void 0 : _a.get(binding);
126 this.emit(event, { ...ctxEvent, cachedValue });
127 }
128 else {
129 this.emit(event, ctxEvent);
130 }
131 this.refresh();
132 }
133 /**
134 * Refresh the view by invalidating its cache
135 */
136 refresh() {
137 debug('Refreshing the view by invalidating cache');
138 this._cachedBindings = undefined;
139 this._cachedValues = undefined;
140 this.emit('refresh');
141 }
142 /**
143 * Resolve values for the matching bindings
144 * @param session - Resolution session
145 */
146 resolve(session) {
147 debug('Resolving values');
148 if (this._cachedValues != null) {
149 return this.getCachedValues();
150 }
151 const bindings = this.bindings;
152 let result = (0, value_promise_1.resolveList)(bindings, b => {
153 const options = {
154 ...this.resolutionOptions,
155 ...(0, resolution_session_1.asResolutionOptions)(session),
156 };
157 // https://github.com/loopbackio/loopback-next/issues/9041
158 // We should start with a new session for `view` resolution to avoid
159 // possible circular dependencies
160 options.session = undefined;
161 return b.getValue(this.context, options);
162 });
163 if ((0, value_promise_1.isPromiseLike)(result)) {
164 result = result.then(values => {
165 const list = values.filter(v => v != null);
166 this.updateCachedValues(list);
167 this.emit('resolve', list);
168 return list;
169 });
170 }
171 else {
172 // Clone the array so that the cached values won't be mutated
173 const list = (result = result.filter(v => v != null));
174 this.updateCachedValues(list);
175 this.emit('resolve', list);
176 }
177 return result;
178 }
179 /**
180 * Get the list of resolved values. If they are not cached, it tries to find
181 * and resolve them.
182 */
183 async values(session) {
184 debug('Reading values');
185 // Wait for the next tick so that context event notification can be emitted
186 await new Promise(resolve => {
187 process.nextTick(() => resolve());
188 });
189 if (this._cachedValues == null) {
190 return this.resolve(session);
191 }
192 return this.getCachedValues();
193 }
194 /**
195 * As a `Getter` function
196 */
197 asGetter(session) {
198 return () => this.values(session);
199 }
200 /**
201 * Get the single value
202 */
203 async singleValue(session) {
204 const values = await this.values(session);
205 if (values.length === 0)
206 return undefined;
207 if (values.length === 1)
208 return values[0];
209 throw new Error('The ContextView has more than one value. Use values() to access them.');
210 }
211 // eslint-disable-next-line @typescript-eslint/no-explicit-any
212 on(event, listener) {
213 return super.on(event, listener);
214 }
215 // eslint-disable-next-line @typescript-eslint/no-explicit-any
216 once(event, listener) {
217 return super.once(event, listener);
218 }
219}
220exports.ContextView = ContextView;
221/**
222 * Create a context view as a getter
223 * @param ctx - Context object
224 * @param bindingFilter - A function to match bindings
225 * @param bindingComparatorOrSession - A function to sort matched bindings or
226 * resolution session if the comparator is not needed
227 * @param session - Resolution session if the comparator is provided
228 */
229function createViewGetter(ctx, bindingFilter, bindingComparatorOrSession, session) {
230 let bindingComparator = undefined;
231 if (typeof bindingComparatorOrSession === 'function') {
232 bindingComparator = bindingComparatorOrSession;
233 }
234 else if (bindingComparatorOrSession instanceof resolution_session_1.ResolutionSession) {
235 session = bindingComparatorOrSession;
236 }
237 const options = (0, resolution_session_1.asResolutionOptions)(session);
238 const view = new ContextView(ctx, bindingFilter, bindingComparator, options);
239 view.open();
240 return view.asGetter(options);
241}
242exports.createViewGetter = createViewGetter;
243//# sourceMappingURL=context-view.js.map
\No newline at end of file