UNPKG

9.99 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2018,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.ResolutionError = exports.asResolutionOptions = exports.ResolutionSession = void 0;
8const tslib_1 = require("tslib");
9const metadata_1 = require("@loopback/metadata");
10const debug_1 = tslib_1.__importDefault(require("debug"));
11const value_promise_1 = require("./value-promise");
12const debugSession = (0, debug_1.default)('loopback:context:resolver:session');
13const getTargetName = metadata_1.DecoratorFactory.getTargetName;
14/**
15 * Type guard for binding elements
16 * @param element - A resolution element
17 */
18function isBinding(element) {
19 return element != null && element.type === 'binding';
20}
21/**
22 * Type guard for injection elements
23 * @param element - A resolution element
24 */
25function isInjection(element) {
26 return element != null && element.type === 'injection';
27}
28/**
29 * Object to keep states for a session to resolve bindings and their
30 * dependencies within a context
31 */
32class ResolutionSession {
33 constructor() {
34 /**
35 * A stack of bindings for the current resolution session. It's used to track
36 * the path of dependency resolution and detect circular dependencies.
37 */
38 this.stack = [];
39 }
40 /**
41 * Fork the current session so that a new one with the same stack can be used
42 * in parallel or future resolutions, such as multiple method arguments,
43 * multiple properties, or a getter function
44 * @param session - The current session
45 */
46 static fork(session) {
47 if (session === undefined)
48 return undefined;
49 const copy = new ResolutionSession();
50 copy.stack.push(...session.stack);
51 return copy;
52 }
53 /**
54 * Run the given action with the given binding and session
55 * @param action - A function to do some work with the resolution session
56 * @param binding - The current binding
57 * @param session - The current resolution session
58 */
59 static runWithBinding(action, binding, session = new ResolutionSession()) {
60 // Start to resolve a binding within the session
61 session.pushBinding(binding);
62 return (0, value_promise_1.tryWithFinally)(() => action(session), () => session.popBinding());
63 }
64 /**
65 * Run the given action with the given injection and session
66 * @param action - A function to do some work with the resolution session
67 * @param binding - The current injection
68 * @param session - The current resolution session
69 */
70 static runWithInjection(action, injection, session = new ResolutionSession()) {
71 session.pushInjection(injection);
72 return (0, value_promise_1.tryWithFinally)(() => action(session), () => session.popInjection());
73 }
74 /**
75 * Describe the injection for debugging purpose
76 * @param injection - Injection object
77 */
78 static describeInjection(injection) {
79 const name = getTargetName(injection.target, injection.member, injection.methodDescriptorOrParameterIndex);
80 return {
81 targetName: name,
82 bindingSelector: injection.bindingSelector,
83 metadata: injection.metadata,
84 };
85 }
86 /**
87 * Push the injection onto the session
88 * @param injection - Injection The current injection
89 */
90 pushInjection(injection) {
91 /* istanbul ignore if */
92 if (debugSession.enabled) {
93 debugSession('Enter injection:', ResolutionSession.describeInjection(injection));
94 }
95 this.stack.push({ type: 'injection', value: injection });
96 /* istanbul ignore if */
97 if (debugSession.enabled) {
98 debugSession('Resolution path:', this.getResolutionPath());
99 }
100 }
101 /**
102 * Pop the last injection
103 */
104 popInjection() {
105 const top = this.stack.pop();
106 if (!isInjection(top)) {
107 throw new Error('The top element must be an injection');
108 }
109 const injection = top.value;
110 /* istanbul ignore if */
111 if (debugSession.enabled) {
112 debugSession('Exit injection:', ResolutionSession.describeInjection(injection));
113 debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
114 }
115 return injection;
116 }
117 /**
118 * Getter for the current injection
119 */
120 get currentInjection() {
121 for (let i = this.stack.length - 1; i >= 0; i--) {
122 const element = this.stack[i];
123 if (isInjection(element))
124 return element.value;
125 }
126 return undefined;
127 }
128 /**
129 * Getter for the current binding
130 */
131 get currentBinding() {
132 for (let i = this.stack.length - 1; i >= 0; i--) {
133 const element = this.stack[i];
134 if (isBinding(element))
135 return element.value;
136 }
137 return undefined;
138 }
139 /**
140 * Enter the resolution of the given binding. If
141 * @param binding - Binding
142 */
143 pushBinding(binding) {
144 /* istanbul ignore if */
145 if (debugSession.enabled) {
146 debugSession('Enter binding:', binding.toJSON());
147 }
148 if (this.stack.find(i => isBinding(i) && i.value === binding)) {
149 const msg = `Circular dependency detected: ` +
150 `${this.getResolutionPath()} --> ${binding.key}`;
151 debugSession(msg);
152 throw new Error(msg);
153 }
154 this.stack.push({ type: 'binding', value: binding });
155 /* istanbul ignore if */
156 if (debugSession.enabled) {
157 debugSession('Resolution path:', this.getResolutionPath());
158 }
159 }
160 /**
161 * Exit the resolution of a binding
162 */
163 popBinding() {
164 const top = this.stack.pop();
165 if (!isBinding(top)) {
166 throw new Error('The top element must be a binding');
167 }
168 const binding = top.value;
169 /* istanbul ignore if */
170 if (debugSession.enabled) {
171 debugSession('Exit binding:', binding === null || binding === void 0 ? void 0 : binding.toJSON());
172 debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
173 }
174 return binding;
175 }
176 /**
177 * Getter for bindings on the stack
178 */
179 get bindingStack() {
180 return this.stack.filter(isBinding).map(e => e.value);
181 }
182 /**
183 * Getter for injections on the stack
184 */
185 get injectionStack() {
186 return this.stack.filter(isInjection).map(e => e.value);
187 }
188 /**
189 * Get the binding path as `bindingA --> bindingB --> bindingC`.
190 */
191 getBindingPath() {
192 return this.stack.filter(isBinding).map(describe).join(' --> ');
193 }
194 /**
195 * Get the injection path as `injectionA --> injectionB --> injectionC`.
196 */
197 getInjectionPath() {
198 return this.injectionStack
199 .map(i => ResolutionSession.describeInjection(i).targetName)
200 .join(' --> ');
201 }
202 /**
203 * Get the resolution path including bindings and injections, for example:
204 * `bindingA --> @ClassA[0] --> bindingB --> @ClassB.prototype.prop1
205 * --> bindingC`.
206 */
207 getResolutionPath() {
208 return this.stack.map(describe).join(' --> ');
209 }
210 toString() {
211 return this.getResolutionPath();
212 }
213}
214exports.ResolutionSession = ResolutionSession;
215function describe(e) {
216 switch (e.type) {
217 case 'injection':
218 return '@' + ResolutionSession.describeInjection(e.value).targetName;
219 case 'binding':
220 return e.value.key;
221 }
222}
223/**
224 * Normalize ResolutionOptionsOrSession to ResolutionOptions
225 * @param optionsOrSession - resolution options or session
226 */
227function asResolutionOptions(optionsOrSession) {
228 // backwards compatibility
229 if (optionsOrSession instanceof ResolutionSession) {
230 return { session: optionsOrSession };
231 }
232 return optionsOrSession !== null && optionsOrSession !== void 0 ? optionsOrSession : {};
233}
234exports.asResolutionOptions = asResolutionOptions;
235/**
236 * Error for context binding resolutions and dependency injections
237 */
238class ResolutionError extends Error {
239 constructor(message, resolutionCtx) {
240 super(ResolutionError.buildMessage(message, resolutionCtx));
241 this.resolutionCtx = resolutionCtx;
242 this.name = ResolutionError.name;
243 }
244 static buildDetails(resolutionCtx) {
245 var _a, _b, _c, _d, _e, _f, _g;
246 return {
247 context: (_b = (_a = resolutionCtx.context) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '',
248 binding: (_d = (_c = resolutionCtx.binding) === null || _c === void 0 ? void 0 : _c.key) !== null && _d !== void 0 ? _d : '',
249 resolutionPath: (_g = (_f = (_e = resolutionCtx.options) === null || _e === void 0 ? void 0 : _e.session) === null || _f === void 0 ? void 0 : _f.getResolutionPath()) !== null && _g !== void 0 ? _g : '',
250 };
251 }
252 /**
253 * Build the error message for the resolution to include more contextual data
254 * @param reason - Cause of the error
255 * @param resolutionCtx - Resolution context
256 */
257 static buildMessage(reason, resolutionCtx) {
258 const info = this.describeResolutionContext(resolutionCtx);
259 const message = `${reason} (${info})`;
260 return message;
261 }
262 static describeResolutionContext(resolutionCtx) {
263 const details = ResolutionError.buildDetails(resolutionCtx);
264 const items = [];
265 for (const [name, val] of Object.entries(details)) {
266 if (val !== '') {
267 items.push(`${name}: ${val}`);
268 }
269 }
270 return items.join(', ');
271 }
272}
273exports.ResolutionError = ResolutionError;
274//# sourceMappingURL=resolution-session.js.map
\No newline at end of file