Skip to content

Commit

Permalink
feat(phase): use handler chain to control how to proceed
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Dec 8, 2018
1 parent dffc44d commit b5faf1a
Show file tree
Hide file tree
Showing 11 changed files with 756 additions and 176 deletions.
105 changes: 105 additions & 0 deletions packages/phase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,111 @@ https://github.com/strongloop/loopback-phase.
npm install --save @loopback/phase
```

## Basic Use

### Handler

A handler is a function that takes a `Context` object, an optional
`HandlerChain` object and returns a `Promise`.

When a handler is invoked as part of a chain, it can control how to proceed as
follows:

- Continue to invoke downstream handlers after it exits

```ts
async ctx => {
console.log(ctx.req.url);
};
```

- Invoke downstream handlers within the method in cascading style

```ts
async (ctx, chain) => {
const start = process.hrtime();
await chain!.next();
const duration = process.hrtime(start);
console.log('Duration: ', duration);
};
```

- Terminate the handler chain and return

```ts
async (ctx, chain) => {
if (ctx.req.url === '/status') {
ctx.response = {status: 'ok'};
chain!.return();
}
};
```

- Abort the handler chain by throwing an error

```ts
async (ctx, chain) => {
throw new Error('invalid request');
// or
// chain!.throw(new Error('invalid request'));
};
```

### Phase

A `Phase` is a bucket for organizing handlers. Each phase has an `id` and three
sub-phases:

- before (contains handlers to be invoked before the phase)
- use (contains handlers to be invoked during the phase)
- after (contains handlers to be invoked after the phase)

The handlers within the same subphase will be executed in serial or parallel
depending on the `options.parallel` flag, which is default to `false`.

The three sub-phases within the same phase will be executed in the order of
`before`, `use`, and `after`.

There is also a `failFast` option to control how to handle errors. If `failFast`
is set to `false`, errors will be caught and set on the `ctx.error` without
aborting the handler chain.

### PhaseList

A `PhaseList` is an ordered list of phases. Each phase is uniquely identified by
its id within the same `PhaseList`. The `PhaseList` provides methods to manage
phases, such as adding a phase before or after another phase.

In addition to the regular phases, each `PhaseList` has two built-in phases:

- errorPhase (`$error`)
- finalPhase (`$final`)

The PhaseList is a chain of grouped list of handlers. When the handler chain is
invoked with a given context, it passes control to handlers registered for each
regular phase one by one sequentially until a handler changes the process by
cascading, returning, or aborting. The flow is very similar as:

```ts
try {
// await run handlers for phase 1
// await run handlers for phase 2
// ...
// await run handlers for phase N
} catch (e) {
// await run handlers for errorPhase
} finally {
// await run handlers for finalPhase
}
```

### Pass information across handlers

The `Context` object can be used to pass data across handlers following the
handler chain so that downstream handlers can access such information populated
by upstream handlers. For cascading handlers, it's also possible to receive data
from downstream handlers after calling `await ctx.handlerChain.next()`.

## Contributions

- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)
Expand Down
1 change: 1 addition & 0 deletions packages/phase/docs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"content": [
"index.ts",
"src/handler.ts",
"src/index.ts",
"src/phase.ts",
"src/phase-list.ts",
Expand Down
7 changes: 5 additions & 2 deletions packages/phase/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@loopback/phase",
"version": "0.1.0",
"description": "LoopBack's API Explorer",
"description": "Phase based handler chain",
"engines": {
"node": ">=8.9"
},
Expand All @@ -21,11 +21,14 @@
"author": "IBM",
"copyright.owner": "IBM Corp.",
"license": "MIT",
"dependencies": {},
"dependencies": {
"debug": "^3.1.0"
},
"devDependencies": {
"@loopback/build": "^0.7.1",
"@loopback/dist-util": "^0.3.6",
"@loopback/testlab": "^0.11.5",
"@types/debug": "0.0.30",
"@types/node": "^10.9.4"
},
"keywords": [
Expand Down
154 changes: 154 additions & 0 deletions packages/phase/src/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright IBM Corp. 2014. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

const debug = require('debug')('loopback:phase');

// tslint:disable:no-any

/**
* Context for the invocation of handlers by phase
*/
export interface Context {
[name: string]: any;
/**
* Error caught during the invocation
*/
error?: any;
}

/**
* Options for execution of handlers
*/
export interface ExecutionOptions {
/**
* Controls if handlers will be executed in parallel
*/
parallel?: boolean;

/**
* Fail on first error, default to `true`. This flag is only honored if
* `parallel` `false`.
*/
failFast?: boolean;
}

/**
* A function that takes the context
*/
export interface Runnable {
/**
* @param ctx: Context
*/
(ctx: Context): Promise<void> | void;
}

/**
* Handler function to be executed within a chain
*/
export interface Handler {
/**
* @param ctx: Context
* @param chain: Optional handler chain if the handler is invoked as part of
* an invocation chain of handlers
*/
(ctx: Context, chain: HandlerChain): Promise<void> | void;
description?: string;
}

/**
* Handler chain that allows its handlers to control how to proceed:
*
* - next: call downstream handlers
* - stop: mark the handler chain is done
* - throw: throw an error to abort the handler chain
*/
export class HandlerChain {
/**
* Indicate if the end of invocation chain is reached or the work is done
*/
done?: boolean;

constructor(private ctx: Context, private handlers: Handler[]) {
this.done = false;
}

/**
* Run the downstream handlers
* @param ctx Optional context, default to `this.ctx`
*/
async next(ctx: Context = this.ctx): Promise<void> {
if (this.done) {
throw new Error('The handler chain is done.');
}
// Mark the chain as `done` because the handler has already called `next`
// to run downstream handlers
this.stop();
debug('Dispatch to downstream handlers (%s)', this);
if (this.handlers.length > 1) {
await asRunnable(this.handlers.slice(1))(ctx);
}
}

/**
* Mark the context as `done` to stop execution of the handler chain
*/
stop(): void {
debug('Stop the handler chain (%s)', this);
this.done = true;
}

/**
* Throw an error to abort execution of the handler chain
* @param err Error
*/
throw(err: any): void {
debug('Throw an error to abort the handler chain (%s)', this, err);
throw err;
}

toString() {
const chain = this.handlers.map(h => h.description || '');
const current = chain[0] || '<END>';
return `${chain.join('->')}: ${current}`;
}
}

/**
* Create a function that executes the given handlers sequentially
* @param handlers Array of handlers
*/
export function asRunnable(
handlers: Handler[],
options: ExecutionOptions = {},
): Runnable {
return async (ctx: Context) => {
if (options.parallel) {
const chain = new HandlerChain(ctx, []);
debug('Executing handlers in parallel');
await Promise.all(handlers.map(h => h(ctx, chain)));
return;
}
for (let i = 0; i < handlers.length; i++) {
const handlerChain = new HandlerChain(ctx, handlers.slice(i));
const handler = handlers[i];
try {
debug('Executing handler %d: %s', i, handler.description);
await handler(ctx, handlerChain);
} catch (e) {
debug('Error throw from %s', handler.description, e);
if (options.failFast !== false) {
throw e;
}
debug('Error is caught and set as ctx.error', e);
ctx.error = e;
}
if (handlerChain.done === true) {
debug('Handler chain is marked as done: %s', handlerChain);
return;
}
}
debug('End of handler chain is reached: %s');
};
}
3 changes: 2 additions & 1 deletion packages/phase/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright IBM Corp. 2014. All Rights Reserved.
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './handler';
export * from './phase';
export * from './phase-list';
2 changes: 1 addition & 1 deletion packages/phase/src/merge-name-lists.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright IBM Corp. 2014. All Rights Reserved.
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
Expand Down
Loading

0 comments on commit b5faf1a

Please sign in to comment.