Skip to content

Commit

Permalink
refactor(context): extract value/promise types/methods to its own file
Browse files Browse the repository at this point in the history
- BoundValue/ValueOrPromise
- isPromise
- getDeepProperty
- resolveList/resolveMap
  - There are a few places that we need to resolve entries of a map or
    list.Some of the entries can be resolved synchronously while others
    need to be resolved asynchronously. For example, multiple arguments
    of a constructor or a method, multiple properties of a class, and
    multiple bindings of @inject.tag.
  • Loading branch information
raymondfeng committed Jan 19, 2018
1 parent 0df1e0d commit dc8c16d
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 171 deletions.
7 changes: 1 addition & 6 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@
import {Context} from './context';
import {ResolutionSession} from './resolution-session';
import {Constructor, instantiateClass} from './resolver';
import {isPromise} from './is-promise';
import {isPromise, BoundValue, ValueOrPromise} from './value-promise';
import {Provider} from './provider';

import * as debugModule from 'debug';
const debug = debugModule('loopback:context:binding');

// tslint:disable-next-line:no-any
export type BoundValue = any;

export type ValueOrPromise<T> = T | Promise<T>;

/**
* Scope for binding values
*/
Expand Down
25 changes: 7 additions & 18 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, BoundValue, ValueOrPromise} from './binding';
import {isPromise} from './is-promise';
import {Binding} from './binding';
import {
isPromise,
BoundValue,
ValueOrPromise,
getDeepProperty,
} from './value-promise';
import {ResolutionSession} from './resolution-session';

import * as debugModule from 'debug';
Expand Down Expand Up @@ -276,19 +281,3 @@ export class Context {
return json;
}
}

/**
* Get nested properties by path
* @param value Value of an object
* @param path Path to the property
*/
function getDeepProperty(value: BoundValue, path: string) {
const props = path.split('.');
for (const p of props) {
value = value[p];
if (value === undefined || value === null) {
return value;
}
}
return value;
}
19 changes: 11 additions & 8 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export {
Binding,
BindingScope,
BindingType,
BoundValue,
ValueOrPromise,
} from './binding';
export {Binding, BindingScope, BindingType} from './binding';

export {Context} from './context';
export {Constructor} from './resolver';
export {ResolutionSession} from './resolution-session';
export {inject, Setter, Getter} from './inject';
export {Provider} from './provider';
export {isPromise} from './is-promise';
export {
isPromise,
BoundValue,
ValueOrPromise,
MapObject,
resolveList,
resolveMap,
tryWithFinally,
getDeepProperty,
} from './value-promise';

// internals for testing
export {instantiateClass, invokeMethod} from './resolver';
Expand Down
30 changes: 9 additions & 21 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import {
PropertyDecoratorFactory,
MetadataMap,
} from '@loopback/metadata';
import {BoundValue, ValueOrPromise} from './binding';
import {
BoundValue,
ValueOrPromise,
isPromise,
resolveList,
} from './value-promise';
import {Context} from './context';
import {ResolutionSession} from './resolution-session';
import {isPromise} from './is-promise';

const PARAMETERS_KEY = 'inject:parameters';
const PROPERTIES_KEY = 'inject:properties';
Expand Down Expand Up @@ -249,28 +253,12 @@ function resolveByTag(
) {
const tag: string | RegExp = injection.metadata!.tag;
const bindings = ctx.findByTag(tag);
const values: BoundValue[] = new Array(bindings.length);

// A closure to set a value by index
const valSetter = (i: number) => (val: BoundValue) => (values[i] = val);

let asyncResolvers: PromiseLike<BoundValue>[] = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < bindings.length; i++) {
return resolveList(bindings, b => {
// We need to clone the session so that resolution of multiple bindings
// can be tracked in parallel
const val = bindings[i].getValue(ctx, ResolutionSession.fork(session));
if (isPromise(val)) {
asyncResolvers.push(val.then(valSetter(i)));
} else {
values[i] = val;
}
}
if (asyncResolvers.length) {
return Promise.all(asyncResolvers).then(vals => values);
} else {
return values;
}
return b.getValue(ctx, ResolutionSession.fork(session));
});
}

/**
Expand Down
18 changes: 0 additions & 18 deletions packages/context/src/is-promise.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/context/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {ValueOrPromise} from './binding';
import {ValueOrPromise} from './value-promise';

/**
* Providers allow developers to compute injected values dynamically,
Expand Down
46 changes: 7 additions & 39 deletions packages/context/src/resolution-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, ValueOrPromise, BoundValue} from './binding';
import {Binding} from './binding';
import {Injection} from './inject';
import {isPromise} from './is-promise';
import {
isPromise,
ValueOrPromise,
BoundValue,
tryWithFinally,
} from './value-promise';
import * as debugModule from 'debug';
import {DecoratorFactory} from '@loopback/metadata';

Expand All @@ -19,43 +24,6 @@ export type ResolutionAction = (
session?: ResolutionSession,
) => ValueOrPromise<BoundValue>;

/**
* Try to run an action that returns a promise or a value
* @param action A function that returns a promise or a value
* @param finalAction A function to be called once the action
* is fulfilled or rejected (synchronously or asynchronously)
*/
function tryWithFinally(
action: () => ValueOrPromise<BoundValue>,
finalAction: () => void,
) {
let result: ValueOrPromise<BoundValue>;
try {
result = action();
} catch (err) {
finalAction();
throw err;
}
if (isPromise(result)) {
// Once (promise.finally)[https://github.com/tc39/proposal-promise-finally
// is supported, the following can be simplifed as
// `result = result.finally(finalAction);`
result = result.then(
val => {
finalAction();
return val;
},
err => {
finalAction();
throw err;
},
);
} else {
finalAction();
}
return result;
}

/**
* Wrapper for bindings tracked by resolution sessions
*/
Expand Down
81 changes: 26 additions & 55 deletions packages/context/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@

import {DecoratorFactory} from '@loopback/metadata';
import {Context} from './context';
import {BoundValue, ValueOrPromise} from './binding';
import {isPromise} from './is-promise';
import {
BoundValue,
ValueOrPromise,
MapObject,
isPromise,
resolveList,
resolveMap,
} from './value-promise';

import {
describeInjectedArguments,
describeInjectedProperties,
Expand Down Expand Up @@ -45,7 +52,7 @@ export function instantiateClass<T>(
session?: ResolutionSession,
// tslint:disable-next-line:no-any
nonInjectedArgs?: any[],
): T | Promise<T> {
): ValueOrPromise<T> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Instantiating %s', getTargetName(ctor));
Expand All @@ -55,7 +62,7 @@ export function instantiateClass<T>(
}
const argsOrPromise = resolveInjectedArguments(ctor, '', ctx, session);
const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session);
let inst: T | Promise<T>;
let inst: ValueOrPromise<T>;
if (isPromise(argsOrPromise)) {
// Instantiate the class asynchronously
inst = argsOrPromise.then(args => {
Expand Down Expand Up @@ -159,7 +166,7 @@ export function resolveInjectedArguments(
session?: ResolutionSession,
// tslint:disable-next-line:no-any
nonInjectedArgs?: any[],
): BoundValue[] | Promise<BoundValue[]> {
): ValueOrPromise<BoundValue[]> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Resolving injected arguments for %s', getTargetName(target, method));
Expand All @@ -176,20 +183,19 @@ export function resolveInjectedArguments(
// Example value:
// [ , 'key1', , 'key2']
const injectedArgs = describeInjectedArguments(target, method);
nonInjectedArgs = nonInjectedArgs || [];
const extraArgs = nonInjectedArgs || [];

const argLength = DecoratorFactory.getNumberOfParameters(target, method);
const args: BoundValue[] = new Array(argLength);
let asyncResolvers: Promise<void>[] | undefined = undefined;

let nonInjectedIndex = 0;
for (let ix = 0; ix < argLength; ix++) {
return resolveList(new Array(argLength), (val, ix) => {
// The `val` argument is not used as the resolver only uses `injectedArgs`
// and `extraArgs` to return the new value
const injection = ix < injectedArgs.length ? injectedArgs[ix] : undefined;
if (injection == null || (!injection.bindingKey && !injection.resolve)) {
if (nonInjectedIndex < nonInjectedArgs.length) {
if (nonInjectedIndex < extraArgs.length) {
// Set the argument from the non-injected list
args[ix] = nonInjectedArgs[nonInjectedIndex++];
continue;
return extraArgs[nonInjectedIndex++];
} else {
const name = getTargetName(target, method, ix);
throw new Error(
Expand All @@ -200,27 +206,13 @@ export function resolveInjectedArguments(
}
}

// Clone the session so that multiple arguments can be resolved in parallel
const valueOrPromise = resolve(
return resolve(
ctx,
injection,
// Clone the session so that multiple arguments can be resolved in parallel
ResolutionSession.fork(session),
);
if (isPromise(valueOrPromise)) {
if (!asyncResolvers) asyncResolvers = [];
asyncResolvers.push(
valueOrPromise.then((v: BoundValue) => (args[ix] = v)),
);
} else {
args[ix] = valueOrPromise as BoundValue;
}
}

if (asyncResolvers) {
return Promise.all(asyncResolvers).then(() => args);
} else {
return args;
}
});
}

/**
Expand Down Expand Up @@ -277,8 +269,6 @@ export function invokeMethod(
}
}

export type KV = {[p: string]: BoundValue};

/**
* Given a class with properties decorated with `@inject`,
* return the map of properties resolved using the values
Expand All @@ -295,21 +285,14 @@ export function resolveInjectedProperties(
constructor: Function,
ctx: Context,
session?: ResolutionSession,
): KV | Promise<KV> {
): ValueOrPromise<MapObject<BoundValue>> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Resolving injected properties for %s', getTargetName(constructor));
}
const injectedProperties = describeInjectedProperties(constructor.prototype);

const properties: KV = {};
let asyncResolvers: Promise<void>[] | undefined = undefined;

const propertyResolver = (p: string) => (v: BoundValue) =>
(properties[p] = v);

for (const p in injectedProperties) {
const injection = injectedProperties[p];
return resolveMap(injectedProperties, (injection, p) => {
if (!injection.bindingKey && !injection.resolve) {
const name = getTargetName(constructor, p);
throw new Error(
Expand All @@ -318,23 +301,11 @@ export function resolveInjectedProperties(
);
}

// Clone the session so that multiple properties can be resolved in parallel
const valueOrPromise = resolve(
return resolve(
ctx,
injection,
// Clone the session so that multiple properties can be resolved in parallel
ResolutionSession.fork(session),
);
if (isPromise(valueOrPromise)) {
if (!asyncResolvers) asyncResolvers = [];
asyncResolvers.push(valueOrPromise.then(propertyResolver(p)));
} else {
properties[p] = valueOrPromise as BoundValue;
}
}

if (asyncResolvers) {
return Promise.all(asyncResolvers).then(() => properties);
} else {
return properties;
}
});
}
Loading

0 comments on commit dc8c16d

Please sign in to comment.