Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Port loopback-phase to TypeScript for LoopBack 4 #1671

Closed
wants to merge 2 commits into from

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Sep 5, 2018

This PR ports loopback-phase to TypeScript with improvements:

  • Promise based handler
  • Allow the handler to control how to proceed if it's invoked as part of the invocation chain (supporting both express and koa styles)
  • Special error and final phase to mimic try - catch - finally

This package can be used as the foundation for the use cases such as:

  • Express middleware registration/execution by phase
  • Sequence of action for req/res processing
  • Sequence of boot/lifecycle steps

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

@raymondfeng raymondfeng requested a review from bajtos as a code owner September 5, 2018 20:26
@raymondfeng raymondfeng force-pushed the phase branch 2 times, most recently from 72d8eb9 to 0d9b816 Compare September 5, 2018 20:49
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if phases is the right design approach, but don't mind experimenting in this area.

I think this work is out of 4.0 GA scope, could you please confirm?

```ts
async (ctx, chain) => {
const start = process.hrtime();
await chain!.next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please find a better type description for the callback function, one that does not require users to use ! operator. If the framework always provide chain value, then this fact should be encoded in the type signature of the callback.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find return as ambiguous. Can we use a name that better expresses the intent please? For example: abort, terminate or stop.


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this rather dangerous. I expect that most people writing phase-handlers are not going to be aware of the failFast:false mode. As a result, failFast:false mode can execute handlers that are not prepared to handle the situation left after one of the previous handlers failed.

I think a better solution would be to enable failFast:false mode on a per-handler basis - i.e. allow each handler to signal whether it wants to be executed even if there was an error up in the chain.

What is the use case you are envisioning for failFast:false mode?

Can we perhaps leave this feature out of this initial pull request, to get it landed faster?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I introduce failFast:false mode mostly for error phase so that we can have a list of error handlers to mediate errors.

} catch (e) {
// await run handlers for errorPhase
} finally {
// await run handlers for finalPhase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we enforcing the order of $error and $final phase? Can users insert additional phases before, in-between or after these two built-in phases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we enforce them as catch and finally. No additional phases can interfere with them.

{
"name": "@loopback/phase",
"version": "0.1.0",
"description": "LoopBack's API Explorer",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the package description.

return async (ctx: Context) => {
if (options.parallel) {
debug('Executing handlers in parallel');
await Promise.all(handlers.map(h => h(ctx)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to run all handlers in parallel.

What if we have 3 handlers registered and the handler no.2 calls ctx.return()? It would be surprised to learn that the 3rd handler was executed to completion too!

What if we have 2 handlers registered and the first handler throws an error? I would find it surprising that the second handler was still executed. Also, are we going to wait until that second handler finishes, or will we return the error immediately?

Also, what's the point of Phase#parallel option when we are always running all handlers in parallel?

Please add tests to capture the expected behavior and verify the implementation.

* @returns phaseNames
*/
getPhaseNames(): string[] {
return this.phases.map(p => p.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include $error and $final in this list?

* @param handler The handler function to register
* with the given phase.
*/
registerHandler(phaseName: string, handler: Handler) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the API for registering handlers for $error and $final phases?

.use(mockHandler('h1'))
.after(mockHandler('a1'))
.after(mockHandler('a2'))
.use(mockHandler('h2'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a more descriptive names, I find it difficult to follow these terse two-letter identifiers.

For example: before-handlerA-1, before-handlerA-2, handlerA, after-handlerA-1, etc. handlerB.

@raymondfeng raymondfeng force-pushed the phase branch 7 times, most recently from 6c84945 to 6c1be3a Compare September 7, 2018 16:14
Copy link
Contributor

@virkt25 virkt25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a quick look. Didn't point out all the places the year / name needs to be updated. Had some questions to get a better understanding of this package. Will do another pass later to go over the tests.

@@ -0,0 +1,154 @@
// Copyright IBM Corp. 2014. All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick / reminder: copyright year and node module need to be updated.

/**
* Context for the invocation of handlers by phase
*/
export interface Context {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general cleanup of this file I think it might be best to introduce a interfaces.ts or types.ts which defines all the interfaces since they are cluttering this file imo.

@@ -0,0 +1,8 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: loopback-phase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder: update module name.

@@ -0,0 +1,320 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: loopback-phase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update module name

* @param phase The phase to be added
*/
add(phase: string | Phase) {
return this.addAll(phase)[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most implementations I've seen, the logic usually lives in the singular form of the function and the plural form is the one that calls the singular form as it loops over the entries. I'm curious to learn as to why this implementation is flipped in this particular case.


### Handler

A handler is a function that takes a `Context` object, an optional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Till I dug into the code, I found the usage of the work Context here confusing as I thought this was referring to @loopback/context.

@bajtos
Copy link
Member

bajtos commented Jan 7, 2019

@raymondfeng what's the status of this pull request - is it still relevant?

@stale
Copy link

stale bot commented Apr 11, 2020

This issue has been marked stale because it has not seen activity within six months. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository. This issue will be closed within 30 days of being stale.

@stale stale bot added the stale label Apr 11, 2020
@raymondfeng
Copy link
Contributor Author

Closing it to favor middleware chain.

@raymondfeng raymondfeng closed this Jun 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants