Skip to content

Components

Austin Keener edited this page Aug 5, 2019 · 17 revisions

Components are modules or plugins that modify or add functionality to the pipeline when they are added to it. Components can be written by consumers of twine to augment pipelines in ways not already included in the library.

Life cycle

A component added to a pipeline via the addComponent(callback) function has 2 phases: Before-Request and After-Request

Before-Request

Before-Request occurs when the pipeline is executed. When .execute() is called, it iterates through the added components in reverse, executing each component's callback function. When it does this, it provides 2 arguments: The Twine Context, and a function to call to invoke the next component in the list.

addComponent((context, next) => {
  ...
  return next()
})

During this phase, the component has the ability to setup aspects of the pipeline's context, storing variables and other things that will influence the request that is made by the the Resource Service.

After the component has completed its setup actions, it should call and return the provided next() function. This function continues the rest of the pipeline. This function provides a Promise which is how a component can access the After-Request life cycle phase.

After-Request

After all components have executed (and executed their provided next() callbacks to continue), the ResourceService will execute and return a Promise. Once this Promise is fulfilled, components will shift into their After-Request phase. A component will have access to this phase by using the .then and .catch methods on the promise returned by next() callback. It is in this phase that a component will have access to the result of the request.

LifeCycle Example:

const twine = require('@inmar/twine-node')
const jsonPlaceholderService = twine.createResourceService('jsonplaceholder.typicode.com')
  .usingHTTPS()

const getTodoTemplate = jsonPlaceholderService.createRequestTemplate('get-todo')
  .withoutInstrumentation() //This can be ignored for now.
  .withMethod('GET')
  .withUriTemplate('/todos/{id}')
  .addComponent((context, next) => {
    // This is the Before-Request Phase
    context['timing.start-time'] = Date.now()

    //If you don't execute next(), the pipeline will not continue.
    //If you don't return next(), the component that proceeded this will not get to
    // enter its After-Request phase
    return next()
        .then(() => {
          // This is the After-Request phase.
          // You still have access to 'context' here which will be updated.
          console.log(`Request Took ${Date.now() - context['timing.start-time']}ms`)
        })
  })

module.exports = {
  getTodo(todoId) {
    return getTodoTemplate.createRequest()
      .withParameters({ id: todoId })
      .createRequest()
  }
}

Extending Twine

In the above example, we built a simple, in-line, timing component. However, we also saw a few library-provided methods which register components for us:

  • RequestTemplate.prototype.withMethod(string)
  • RequestTemplate.prototype.withUriTemplate(string)
  • Request.prototype.withParameters(object)

Each of these methods is modifying the Twine Context in a way that the usingHTTPS Resource Service component understands.

This is the source of the withMethod component.

RequestTemplate.prototype.withMethod = function(method) {
  return this.addComponent((context, next) => {
    context['http.RequestMethod'] = method
    return next()
  })
}

As you can see, it internally is calling addComponent. With that in mind, we could create the following to extend Twine and provide functionality that other templates could easily reuse:

const twine = require('@inmar/twine-node')
const { RequestTemplate } = twine

RequestTemplate.prototype.logRequestDuration = function() {
  return this.addComponent((context, next) => {
    context['timing.start-time'] = Date.now()
    return next()
      .then(() => {
        console.log(`Request Took ${Date.now() - context['timing.start-time']}ms`)
      })
  })
}

Then our Request Template could be simplified to:

const getTodoTemplate = jsonPlaceholderService.createRequestTemplate('get-todo')
  .withoutInstrumentation() //This can be ignored for now.
  .withMethod('GET')
  .withUriTemplate('/todos/{id}')
  .logRequestDuration()

Component Execution Order

For our above example, if we looked internally at the components of the pipeline, we'd find the following:

[
 ()              => //root of twine.
 (context, next) => // usingHTTPS 

 (context, next) => // withMethod
 (context, next) => // withUriTemplate
 (context, next) => // logRequestDuration
 
 (context, next) => // withParameters
]

When execute() is called on the Request created from .createRequest(), the pipeline's array of components begins to execute in reverse. So: withParameters, logRequestDuration, withUriTemplate, withMethod, and finishing with usingHTTPS.

Understanding this ordering is quite important when you need to write components that rely on eachother. In the example, withUriTemplate and withParameters are components which rely on eachother. withParameters accepts an object whose keys and values are used to replace the tokens ({keyName}) that are present in the string provided to withUriTemplate.

  // RequestTemplate component
  .withUriTemplate('/todos/{id}`)

  // Request component
  .withParameters({ id: todoId })

The withParameters function is quite simple. All it does is store the object it was provided into the Twine Context so that the withUriTemplate can retrieve it and use it to replace the tokens.

Request.prototype.withParameters = function(parametersHash) {
  return this.addComponent((context, next) => {
    context.environment['http.RequestParametersHash'] = parametersHash
    return next()
  })
}

And, for added clarity, a greatly simplified implementation of withUriTemplate:

RequestTemplate.prototype.withURITemplate = function(templateUri) {
  return this.addComponent((context, next) => {
    const parametersHash = Object.assign({}, context.environment['http.RequestParametersHash'])
    let path = templateUri

    for (const key in parametersHash) {
      const newPath = path.replace(new RegExp(`\\{${key}\\}`, 'ig'), parametersHash[key])
      if (newPath !== path) {
        delete parametersHash[key]
        path = newPath
      }
    }

    context.environment['http.RequestPath'] = path
    return next()
  })
}

Introduction

Concepts

Clone this wiki locally