Skip to content

Commit

Permalink
chore(context): review - 8
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jan 24, 2019
1 parent ee0e281 commit 3ea2f02
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 130 deletions.
9 changes: 6 additions & 3 deletions packages/context/src/context-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {BindingFilter} from './binding-filter';
import {ValueOrPromise} from './value-promise';

/**
* Context event types
* Context event types. We support `bind` and `unbind` for now but
* keep it open for new types
*/
export type ContextEventType = string;
export type ContextEventType = 'bind' | 'unbind' | string;

/**
* Listeners of context bind/unbind events
Expand All @@ -22,7 +23,9 @@ export interface ContextEventListener {
filter?: BindingFilter;

/**
* Listen on `bind` or `unbind`
* Listen on `bind`, `unbind`, or other events
* @param eventType Context event type
* @param binding The binding as event source
*/
listen(
eventType: ContextEventType,
Expand Down
30 changes: 16 additions & 14 deletions packages/context/src/context-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
} from './context-listener';
import {Getter} from './inject';
import {ResolutionSession} from './resolution-session';
import {resolveList} from './value-promise';
import {resolveList, ValueOrPromise} from './value-promise';
const debug = debugFactory('loopback:context:view');
const nextTick = promisify(process.nextTick);

/**
* `ContextView` provides a view for a given context chain to maintain a live
Expand All @@ -34,27 +35,26 @@ export class ContextView<T = unknown> implements ContextEventListener {
private _subscription: Subscription | undefined;

constructor(
protected readonly ctx: Context,
protected readonly context: Context,
public readonly filter: BindingFilter,
) {}

/**
* Start listening events from the context
*/
open() {
debug('Start listening on changes of context %s', this.ctx.name);
return (this._subscription = this.ctx.subscribe(this));
debug('Start listening on changes of context %s', this.context.name);
return (this._subscription = this.context.subscribe(this));
}

/**
* Stop listening events from the context
*/
close() {
debug('Stop listening on changes of context %s', this.ctx.name);
if (this._subscription && !this._subscription.closed) {
this._subscription.unsubscribe();
this._subscription = undefined;
}
debug('Stop listening on changes of context %s', this.context.name);
if (!this._subscription || this._subscription.closed) return;
this._subscription.unsubscribe();
this._subscription = undefined;
}

/**
Expand All @@ -74,7 +74,7 @@ export class ContextView<T = unknown> implements ContextEventListener {
*/
protected findBindings() {
debug('Finding matching bindings');
this._cachedBindings = this.ctx.find(this.filter);
this._cachedBindings = this.context.find(this.filter);
return this._cachedBindings;
}

Expand All @@ -98,11 +98,13 @@ export class ContextView<T = unknown> implements ContextEventListener {
* Resolve values for the matching bindings
* @param session Resolution session
*/
resolve(session?: ResolutionSession) {
resolve(session?: ResolutionSession): ValueOrPromise<T[]> {
debug('Resolving values');
// We don't cache values with this method
// We don't cache values with this method as it returns `ValueOrPromise`
// for inject `resolve` function to allow `getSync` but `this._cachedValues`
// expects `T[]`
return resolveList(this.bindings, b => {
return b.getValue(this.ctx, ResolutionSession.fork(session));
return b.getValue(this.context, ResolutionSession.fork(session));
});
}

Expand All @@ -113,7 +115,7 @@ export class ContextView<T = unknown> implements ContextEventListener {
async values(): Promise<T[]> {
debug('Reading values');
// Wait for the next tick so that context event notification can be emitted
await promisify(process.nextTick)();
await nextTick();
if (this._cachedValues == null) {
this._cachedValues = await this.resolve();
}
Expand Down
32 changes: 17 additions & 15 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ export class Context {
/**
* Create a new context. For example,
* ```ts
* const ctx1 = new Context();
* const ctx2 = new Context(ctx1);
* const ctx3 = new Context('ctx3');
* const ctx4 = new Context(ctx3, 'ctx4');
* // Create a new root context, let the framework to create a unique name
* const rootCtx = new Context();
*
* // Create a new child context inheriting bindings from `rootCtx`
* const childCtx = new Context(rootCtx);
*
* // Create another root context called "application"
* const appCtx = new Context('application');
*
* // Create a new child context called "request" and inheriting bindings
* // from `appCtx`
* const reqCtx = new Context(appCtx, 'request');
* ```
* @param _parent The optional parent context
* @param name Name of the context, if not provided, a `uuid` will be
Expand All @@ -65,13 +73,6 @@ export class Context {
this.name = name || uuidv1();
}

/**
* Get the parent context
*/
get parent() {
return this._parent;
}

/**
* Create a binding with the given key in the context. If a locked binding
* already exists with the same key, an error will be thrown.
Expand Down Expand Up @@ -129,12 +130,13 @@ export class Context {
unbind(key: BindingAddress): boolean {
key = BindingKey.validate(key);
const binding = this.registry.get(key);
// If not found, return `false`
if (binding == null) return false;
if (binding && binding.isLocked)
throw new Error(`Cannot unbind key "${key}" of a locked binding`);
const found = this.registry.delete(key);
this.registry.delete(key);
this.notifyListeners('unbind', binding);
return found;
return true;
}

/**
Expand Down Expand Up @@ -570,14 +572,14 @@ export class Context {
*/
class ContextSubscription implements Subscription {
constructor(
protected ctx: Context,
protected context: Context,
protected listener: ContextEventListener,
) {}

private _closed = false;

unsubscribe() {
this.ctx.unsubscribe(this.listener);
this.context.unsubscribe(this.listener);
this._closed = true;
}

Expand Down
87 changes: 58 additions & 29 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function inject(
resolve?: ResolverFunction,
) {
if (typeof bindingSelector === 'function' && !resolve) {
resolve = resolveByFilter;
resolve = resolveValuesByFilter;
}
metadata = Object.assign({decorator: '@inject'}, metadata);
return function markParameterOrPropertyAsInjected(
Expand Down Expand Up @@ -293,7 +293,7 @@ export namespace inject {
metadata?: InjectionMetadata,
) {
metadata = Object.assign({decorator: '@inject.view'}, metadata);
return inject(bindingFilter, metadata, resolveByFilter);
return inject(bindingFilter, metadata, resolveAsContextView);
};

/**
Expand Down Expand Up @@ -332,7 +332,7 @@ function resolveAsGetter(
}
const bindingSelector = injection.bindingSelector;
if (!isBindingAddress(bindingSelector)) {
return resolveByFilter(ctx, injection, session);
return resolveAsGetterByFilter(ctx, injection, session);
}
// We need to clone the session for the getter as it will be resolved later
session = ResolutionSession.fork(session);
Expand Down Expand Up @@ -420,46 +420,75 @@ function inspectTargetType(injection: Readonly<Injection>) {
}

/**
* Resolve values by a binding filter function
* Resolve an array of bound values matching the filter function for `@inject`.
* @param ctx Context object
* @param injection Injection information
* @param session Resolution session
*/
function resolveByFilter(
function resolveValuesByFilter(
ctx: Context,
injection: Readonly<Injection>,
session?: ResolutionSession,
) {
const targetType = inspectTargetType(injection);
const decorator =
(injection.metadata && injection.metadata.decorator) || '@inject';
const targetName = ResolutionSession.describeInjection(injection)!.targetName;
if (decorator === '@inject.view') {
if (targetType && targetType !== ContextView) {
throw new Error(
`The type of ${targetName} (${targetType.name}) is not ContextView`,
);
}
} else if (decorator === '@inject') {
if (targetType !== Array) {
throw new Error(
`The type of ${targetName} (${targetType.name}) is not Array`,
);
}

if (targetType !== Array) {
const targetName = ResolutionSession.describeInjection(injection)!
.targetName;
throw new Error(
`The type of ${targetName} (${targetType.name}) is not Array`,
);
}

const bindingFilter = injection.bindingSelector as BindingFilter;
const view = new ContextView(ctx, bindingFilter);
const autoOpen = injection.metadata!.autoOpen;
return view.resolve(session);
}

if (targetType === Function) {
if (autoOpen !== false) view.open();
return view.asGetter();
} else if (targetType === ContextView) {
if (autoOpen !== false) view.open();
return view;
} else {
return view.resolve(session);
/**
* Resolve to a getter function that returns an array of bound values matching
* the filter function for `@inject.getter`.
*
* @param ctx Context object
* @param injection Injection information
* @param session Resolution session
*/
function resolveAsGetterByFilter(
ctx: Context,
injection: Readonly<Injection>,
session?: ResolutionSession,
) {
const bindingFilter = injection.bindingSelector as BindingFilter;
const view = new ContextView(ctx, bindingFilter);
view.open();
return view.asGetter();
}

/**
* Resolve to an instance of `ContextView` by the binding filter function
* for `@inject.view`
* @param ctx Context object
* @param injection Injection information
* @param session Resolution session
*/
function resolveAsContextView(
ctx: Context,
injection: Readonly<Injection>,
session?: ResolutionSession,
) {
const targetType = inspectTargetType(injection);
if (targetType && targetType !== ContextView) {
const targetName = ResolutionSession.describeInjection(injection)!
.targetName;
throw new Error(
`The type of ${targetName} (${targetType.name}) is not ContextView`,
);
}

const bindingFilter = injection.bindingSelector as BindingFilter;
const view = new ContextView(ctx, bindingFilter);
view.open();
return view;
}

/**
Expand Down
Loading

0 comments on commit 3ea2f02

Please sign in to comment.