UNPKG

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