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

suya

v1.0.3

Published

A blazing-fast and strongly-typed exxpress middleware(s) that adds caching layer on top of your express API response to improve performance.

Downloads

1

Readme

Suya, a Simple, Fast Cache Middleware(s) for Express ⚡

Suya

Awesome code style: prettier Actions Status MIT License Node version contributions welcome GitHub contributors

Introduction

Suya is an express middleware(s) that adds caching layer on top of your express API response to reduce latency and improve API performance.

Installation

 # through npm
 $ npm i suya
 # through yarn
 $ yarn add suya

Features

  1. Lightweight Library.
  2. Simple API.
  3. Typescript Support.
  4. Many Cache Engines Support.
  5. Nice Terminal Logging.

Usage

To begin, you would need a cache/in memory store such as Redis, or Memcached installed on your machine or alternatively using NodeJS Internal Caching. If you want to quickly get up and running without installing Redis or Memcached on your machine. I highly recommend using managed cloud services like RedisLabs (redis) or Memcachier (memcached). No worries, they both have free plan with 25MB storage with no credit card required.

NodeJS Internal Caching

const express = require('express')
const { Suya } = require('suya')

const app = express()

const Cache = new Suya({
  engine: {
    name: 'node-cache',
  },
})

// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()

// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)

// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
  indicator: {
    success: true,
  },
})

let mockDB = () => {
  let users = [
    {
      id: 1,
      name: 'John Smith',
      email: '[email protected]',
    },
    {
      id: 2,
      name: 'James Noah',
      email: '[email protected]',
    },
  ]

  // mocking response time to be between 100ms - 600ms
  let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100

  return new Promise((resolve, reject) => {
    return setTimeout(() => {
      resolve(users)
    }, randResponseTime)
  })
}

app.get('/users/forever', cacheForever, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

const server = app.listen(2000, () =>
  console.log('Server running at http://127.0.0.1:2000')
)

process.on('unhandledRejection', (err, promise) => {
  console.log(`Error: ${err.message}`)

  // close the server
  server.close(async () => {
    // close connection
    await Cache.close()

    process.exit(1)
  })
})

Redis

const express = require('express')
const { Suya } = require('suya')

const app = express()

const Cache = new Suya({
  engine: {
    name: 'redis',
    configs: {
      redis: {
        // node-redis configs options.
        // https://github.com/NodeRedis/node-redis#options-object-properties
        options: {
          host: '127.0.0.1', // Redis host
          port: 6379, // Redis port
          password: '[pass]', // Redis password
          family: 4, // 4 (IPv4) or 6 (IPv6)
          db: 0, // Redis database
        },
      },
    },
    // whether suya should/shouldn't log to console
    logging: true,
  },
})

// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()

// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)

// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
  indicator: {
    success: true,
  },
})

let mockDB = () => {
  let users = [
    {
      id: 1,
      name: 'John Smith',
      email: '[email protected]',
    },
    {
      id: 2,
      name: 'James Noah',
      email: '[email protected]',
    },
  ]

  // mocking response time to be between 100ms - 600ms
  let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100

  return new Promise((resolve, reject) => {
    return setTimeout(() => {
      resolve(users)
    }, randResponseTime)
  })
}

app.get('/users/forever', cacheForever, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

const server = app.listen(2000, () =>
  console.log('Server running at http://127.0.0.1:2000')
)

process.on('unhandledRejection', (err, promise) => {
  console.log(`Error: ${err.message}`)

  // close the server
  server.close(async () => {
    // close connection
    await Cache.close()

    process.exit(1)
  })
})

Memcached

const express = require('express')
const { Suya } = require('suya')

const app = express()

const Cache = new Suya({
  engine: {
    name: 'memcached',
    configs: {
      memcached: {
        // server string format e.g
        // single server        - user:pass@server1:11211
        // multiple servers     - user:pass@server1:11211,user:pass@server2:11211
        server: 'johndoe:[email protected]:11211', // local memcached server
        // memjs configs options - https://github.com/memcachier/memjs
        // some memjs options are overridden by suya. supported options are
        // {
        //   retries: 2,
        //   retry_delay: 0.2,
        //   failoverTime: 60,
        // }
        options: {
          retries: 2,
          retry_delay: 0.2,
          failoverTime: 60,
        },
      },
    },
    // whether suya should/shouldn't log to console
    logging: true,
  },
})

// This is a middleware to cache forever.
// Methods supported: GET
let cacheForever = Cache.forever()

// This is a middleware to cache for a specific seconds.
// Methods supported: GET
let cacheWithDuration = Cache.duration(50)

// This is a middleware to reset cache on mutation.
// Methods supported: POST, PUT, PATCH, DELETE
let resetCacheOnMutate = Cache.resetOnMutate({
  indicator: {
    success: true,
  },
})

let mockDB = () => {
  let users = [
    {
      id: 1,
      name: 'John Smith',
      email: '[email protected]',
    },
    {
      id: 2,
      name: 'James Noah',
      email: '[email protected]',
    },
  ]

  // mocking response time to be between 100ms - 600ms
  let randResponseTime = Math.floor(Math.random() * 6 + 1) * 100

  return new Promise((resolve, reject) => {
    return setTimeout(() => {
      resolve(users)
    }, randResponseTime)
  })
}

app.get('/users/forever', cacheForever, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/forever', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.get('/users/duration', cacheWithDuration, async (req, res, next) => {
  let users = await mockDB()

  res.status(200).json({
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

app.put('/users/duration', resetCacheOnMutate, async (req, res, next) => {
  let users = await mockDB()

  // res.status(400).json({
  //   // once the indicator set on .resetOnMutate({}) middleware doesn't match
  //   // like so, the data remain cached.
  //   success: false,
  //   error: {
  //     message: 'Email address is required!',
  //   },
  //   code: 400,
  // })

  res.status(200).json({
    // once the indicator set on .resetOnMutate({}) middleware match like so,
    // the cached data would get cleared it out.
    success: true,
    data: {
      users,
    },
    code: 200,
  })
})

const server = app.listen(2000, () =>
  console.log('Server running at http://127.0.0.1:2000')
)

process.on('unhandledRejection', (err, promise) => {
  console.log(`Error: ${err.message}`)

  // close the server
  server.close(async () => {
    // close connection
    await Cache.close()

    process.exit(1)
  })
})

API

  1. let Cache = new Suya({ engine: { name: [name], configs: { [configs] }, logging: [boolean] } })
  • [name] - the name of the in memory engine to use. e.g name: 'node-cache' | 'redis' | 'memcached'

  • [configs] - the configurations for the selected engine. e.g

     // For Node-Cache
     // there's no configurations for node-cache
    
     // For Redis
     configs: {
       redis: {
          options: {
            // node-redis configurations options
            // https://github.com/NodeRedis/node-redis#options-object-properties
          },
       }
     }
    
     // For Memcached
     configs: {
       memcached: {
          // server string format e.g
          // single server        - user:pass@server1:11211
          // multiple servers     - user:pass@server1:11211,user:pass@server2:11211
          server: '',
          // memjs configs options - https://github.com/memcachier/memjs
          // some memjs options are overridden by suya. supported options are
          // {
          //   retries: 2,
          //   retry_delay: 0.2,
          //   failoverTime: 60,
          // }
          options: {},
       }
     }
  • [logging] - whether suya should/shouldn't log to console e.g logging: true | false. Default to true.

  1. Cache.forever() - midddleware to cache forever. Method supported: GET.

  2. Cache.duration([n]) - midddleware to cache for a specific duration (i.e time to live in cache engine) where [n] is the duration in seconds. Method supported: GET.

  3. Cache.resetOnMutate({ indicator: { [key]: [value] } }) - midddleware to reset cache on a successful mutation where [key] and [value] can be any indicator on successful mutation. e.g { success: true }. Method supported: POST, PUT, PATCH, DELETE.

  4. Cache.close() - this is NOT a middleware, its just a helper method to close open connections.

Error Handling

Suya extends the global Error class. Some errors could be handle through express middleware like so:

// global express error handler
app.use((err, req, res, next) => {
  if (err.name == 'SuyaError') {
    return res.status(500).json({
      success: false,
      error: {
        message: err.message,
      },
    })
  }
})

NB: Some errors occur during the initializations of suya object and these errors are thrown when developers don't follow typescript compiler/rules according to suya types definition. These errors are been underlined during the development but the developer ignores them.

Tips

  1. .forever() middleware should be use when your data dont change often and use .resetOnMutate({ indicator: { [key]: [value] } }) to make it upto date on every mutation (POST, PUT, PATCH, DELETE).
  2. .duration([n]) middleware should be use when you are dealing with real time data (data that change often) and don't use .resetOnMutate({ indicator: { [key]: [value] } }) on mutation at all because using will clear up the cache on every mutation (POST, PUT, PATCH, DELETE) hence no performance improvement because the data is real time. .duration([n]) only would get the cache cleared out as the duration elapse.
  3. .close() helper method should be use when node proccess crashes unexpectedly to close open connections to any external resources.

Contributors

Many thanks to all our contributors that helps to add core APIs to suya. I say a BIG thank you.

  1. Obafunso Ridwan Adebayo

Contributions are welcome. Check CONTRIBUTING.md.

Tests

All the benchmark test suites are written with Jest and Axios.

Benchmarks tests

Nodecache

# terminal tab 1
# clone repo
$ git clone [repo_url]

# install dependencies
$ npm i

# start benchmark server
$ npm run start:benchmark:server:node-cache

## ~OUTPUT
## [NODECACHE] Server running at http://localhost:1000

# terminal tab 2
$ npm run benchmark:node-cache

## ~RESULT ON MY MACHINE
#  PASS  tests/benchmarks/node-cache.test.ts (42.591 s)
#    The performance of suya with node-cache
#      √ if there is any performance increase when using node-cache (9106 ms)

#  Test Suites: 1 passed, 1 total
#  Tests:       1 passed, 1 total
#  Snapshots:   0 total
#  Time:        46.997 s

Redis

# terminal tab 1
# clone repo
$ git clone [repo_url]

# install dependencies
$ npm i

# open tests/benchmarks/servers/redis.ts file and update your redis server credentials
# start benchmark server
$ npm run start:benchmark:server:redis

## ~OUTPUT
## [REDIS] Server running at http://localhost:2000
## -----------------------------------------------
## [REDIS] Suya connected to redis successfully!!!
## -----------------------------------------------

# terminal tab 2
$ npm run benchmark:redis

## ~RESULT ON MY MACHINE
#  PASS  tests/benchmarks/redis.test.ts (27.956 s)
#    The performance of suya with redis
#      √ if there is any performance increase when using redis (13092 ms)

#  Test Suites: 1 passed, 1 total
#  Tests:       1 passed, 1 total
#  Snapshots:   0 total
#  Time:        29.912 s

Memcached

# terminal tab 1
# clone repo
$ git clone [repo_url]

# install dependencies
$ npm i

# open tests/benchmarks/servers/memcached.ts file and update your memcached server credentials
# start benchmark server
$ npm run start:benchmark:server:memcached

## ~OUTPUT
## [MEMCACHED] Server running at http://localhost:3000
## -------------------------------------------------------
## [MEMCACHED] Suya connected to memcached successfully!!!
## -------------------------------------------------------

# terminal tab 2
$ npm run benchmark:memcached

## ~RESULT ON MY MACHINE
#  PASS  tests/benchmarks/memcached.test.ts (27.956 s)
#    The performance of suya with memcached
#      √ if there is any performance increase when using memcached (12073 ms)

#  Test Suites: 1 passed, 1 total
#  Tests:       1 passed, 1 total
#  Snapshots:   0 total
#  Time:        28.715 s

Changelog

  • v1.0.3 - Improve documentation and remove dependencies that has it own types built-in i.e @types/colors and @types/node-cache. Created new release with v1.0.3 tag
  • v1.0.2 - Fix bugs of throwing SuyaError in middleware(s) instead of passing the error to the next middleware in the cycle. Created new release with v1.0.2 tag
  • v1.0.1 - Created new release with v1.0.1 tag which triggered Github Actions workflows to format, lint, build and re-publish the library. v1.0.1 is the initial release.
  • v1.0.0 - Unpublished v1.0.0 from npm due to some errors, and remove v1.0.0 releases and tags from this repo.
  • v1.0.0 - Commit all source codes, then I release v1.0.0 tag which triggered Github Actions workflows to format, lint, build and publish the library.

Versioning

I use SemVer for versioning.

License

MIT License

Copyright (c) 2020 [email protected]

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.