npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@opbi/hooks

v1.0.0

Published

<h3 align="center">hooks</h3> <p align="center" style="margin-bottom: 2em;"></p>

Downloads

3

Readme


Purpose

Turn scattered repeatitive control mechanism or observability code from interwined blocks to more readable, reusable, testable ones.

By abstract out common control mechanism and observability code into well-tested, composable hooks, it can effectively half the verboseness of your code. This helps to achieve codebase that is self-explanatory of its business logic and technical behaviour. Additionally, conditionally turning certain mechanism off makes testing the code very handy.

Let's measure the effect in LOC (Line of Code) and LOI (Level of Indent) by an example of cancelling user subscription on server-side with some minimal error handling of retry and restore. The simplification effect will be magnified with increasing complexity of the control mechanism.

Using @opbi/hooks Hooks: LOC = 16, LOI = 2

// import userProfileApi from './api/user-profile';
// import subscriptionApi from './api/subscription';
// import restoreSubscription from './restore-subscription'

import { errorRetry, errorHandler, chain } from '@opbi/hooks';

const retryOnTimeoutError = errorRetry({
  condition: e => e.type === 'TimeoutError'
});

const restoreOnServerError = errorHandler({
  condition: e => e.code > 500,
  handler: (e, p, m, c) => restoreSubscription(p, m, c),
});

const cancelSubscription = async ({ userId }, meta, context) => {
  const { subscriptionId } = await chain(
    retryOnTimeoutError
  )(userProfileApi.getSubscription)( { userId }, meta, context );

  await chain(
    errorRetry(), restoreOnServerError,
  )(subscriptionApi.cancel)({ subscriptionId }, meta, context);
};

// export default cancelSubscription;

Vanilla JavaScript: LOC = 32, LOI = 4

// import userProfileApi from './api/user-profile';
// import subscriptionApi from './api/subscription';
// import restoreSubscription from './restore-subscription'

const cancelSubscription = async({ userId }, meta, context) => {
  let subscriptionId;

  try {
    const result = await userProfileApi.getSubscription({ userId }, meta, context);
    subscriptionId = result.subscriptionId;
  } catch (e) {
    if(e.type === 'TimeoutError'){
      const result = await userProfileApi.getSubscription({ userId }, meta, context);
      subscriptionId = result.subscriptionId;
    }
    throw e;
  }

  try {
    try {
      await subscriptionApi.cancel({ subscriptionId }, meta, context);
    } catch (e) {
      if(e.code > 500) {
        await restoreSubscription({ subscriptionId }, meta, context);
      }
      throw e;
    }
  } catch (e) {
    try {
      return await subscriptionApi.cancel({ subscriptionId }, meta, context);
    } catch (e) {
      if(e.code > 500) {
        await restoreSubscription({ subscriptionId }, meta, context);
      }
      throw e;
    }
  }
}

// export default cancelSubscription;

How to Use

Install

yarn add @opbi/hooks

Standard Function

Standardisation of function signature is powerful that it creates predictable value flows throughout the functions and hooks chain, making functions more friendly to meta-programming. Moreover, it is also now a best-practice to use object destruct assign for key named parameters.

Via exploration and the development of hooks, we set a function signature standard to define the order of different kinds of variables as expected and we call it action function:

/**
 * The standard function signature.
 * @param  {object} param   - parameters input to the function
 * @param  {object} meta    - metadata tagged for function observability(logger, metrics), e.g. requestId
 * @param  {object} context - contextual callable instances or unrecorded metadata, e.g. logger, req
 */
function (param, meta, context) {}

Config the Hooks

All the hooks in @opbi/hooks are configurable with possible default settings.

In the cancelSubscription example, errorRetry() is using its default settings, while restoreOnServerError is configured errorHandler. Descriptive names of hook configurations help to make the behaviour very self-explanatory. Patterns composed of configured hooks can certainly be reused.

const restoreOnServerError = errorHandler({
  condition: e => e.code > 500,
  handler: (e, p, m, c) => restoreSubscription(p, m, c),
});

Chain the Hooks

"The order of the hooks in the chain matters."

Under the hood, the hooks are implemented in the decorators pattern. The pre-hooks, action function, after-hooks/error-hooks are invoked in a pattern as illustrated above. In the cancelSubscription example, as errorRetry(), restoreOnServerError are all error hooks, restoreOnServerError will be invoked first before errorRetry is invoked.


Ecosystem

Currently available hooks:

Hooks are named in a convention to reveal where and how it works [hook point][what it is/does], e.g. errorCounter, eventLogger. Hook points are named before, after, error and event (multiple points).

Extension

You can easily create more standardised hooks with addHooks helper. Open source them aligning with the above standards via pull requests or individual packages are highly encouraged.


Decorators

Hooks here are essentially configurable decorators, while different in the way of usage. We found the name 'hooks' better describe the motion that they are attached to functions not modifying their original data process flow (keep it pure). Decorators are coupled with class methods, while hooks help to decouple definition and control, attaching to any function on demand.

//decorators
class SubscriptionAPI:
  //...
  @errorRetry()
  cancel: () => {}
//hooks
  chain(
    errorRetry()
  )(subscriptionApi.cancel)

Adaptors

To make plugging in @opbi/hooks hooks to existing systems easier, adaptors are introduced to bridge different function signature standards.

const handler = chain(
  adaptorExpress(),
  errorRetry()
)(subscriptionApi.cancel)

handler(req, res, next);

Refactor

To help adopting the hooks by testing them out with minimal refactor on non-standard signature functions, there's an unreleased adaptor to bridge the function signatures. It is not recommended to use this for anything but trying the hooks out, especially observability hooks are not utilised this way.

Reducers

Integration with Redux is TBC.

Pipe Operator

We are excited to see how pipe operator will be rolled out and hooks can be elegantly plugged in.

const cancelSubscription = ({ userId }, meta, context)
  |> chain(timeoutErrorRetry)(userProfileApi.getSubscription)
  |> chain(restoreOnServerError, timeoutErrorRetry)(subscriptionApi.cancel);

Inspiration


License

MIT