Giter VIP home page Giter VIP logo

fastify-caching's Introduction

@fastify/caching

CI NPM version js-standard-style

@fastify/caching is a plugin for the Fastify framework that provides server-side caching and mechanisms for manipulating HTTP cache headers according to RFC 2616 §14.9.

Supports Fastify versions ^3.0.0. Version v5.x supports Fastify ^3.0.0 in the v5.x branch.

This plugin fully supports Fastify's encapsulation. Therefore, routes that should have differing cache settings should be registered within different contexts.

In addition to providing header manipulation, the plugin also decorates the server instance with an object that can be used for caching items. Note: the default cache should not be used in a "production" environment. It is an LRU, in-memory cache that is capped at 100,000 items. It is highly recommended that a full featured cache object be supplied, e.g. abstract-cache-redis.

Example

This example shows using the plugin to disable client side caching of all routes.

const http = require('node:http')
const fastify = require('fastify')()
const fastifyCaching = require('@fastify/caching')

fastify.register(
  fastifyCaching,
  {privacy: fastifyCaching.privacy.NOCACHE},
  (err) => { if (err) throw err }
)

fastify.get('/', (req, reply) => {
  reply.send({hello: 'world'})
})

fastify.listen({ port: 3000 }, (err) => {
  if (err) throw err

  http.get('http://127.0.0.1:3000/', (res) => {
    console.log(res.headers['cache-control'])
  })
})

This example shows how to register the plugin such that it only provides a server-local cache. It will not set any cache control headers on responses. It also shows how to retain a reference to the cache object so that it can be re-used.

const IORedis = require('ioredis')
const redis = new IORedis({host: '127.0.0.1'})
const abcache = require('abstract-cache')({
  useAwait: false,
  driver: {
    name: 'abstract-cache-redis', // must be installed via `npm i`
    options: {client: redis}
  }
})

const fastify = require('fastify')()
fastify
  .register(require('@fastify/redis'), {client: redis})
  .register(require('@fastify/caching'), {cache: abcache})

fastify.get('/', (req, reply) => {
  fastify.cache.set('hello', {hello: 'world'}, 10000, (err) => {
    if (err) return reply.send(err)
    reply.send({hello: 'world'})
  })
})

fastify.listen({ port: 3000 }, (err) => {
  if (err) throw err
})

API

Options

@fastify/caching accepts the options object:

{
  privacy: 'value',
  expiresIn: 300,
  cache: {get, set},
  cacheSegment: 'segment-name'
}
  • privacy (Default: undefined): can be set to any string that is valid for a cache-response-directive as defined by RFC 2616.
  • expiresIn (Default: undefined): a value, in seconds, for the max-age the resource may be cached. When this is set, and privacy is not set to no-cache, then ', max-age=<value>' will be appended to the cache-control header.
  • cache (Default: abstract-cache.memclient): an abstract-cache protocol compliant cache object. Note: the plugin requires a cache instance to properly support the ETag mechanism. Therefore, if a falsy value is supplied the default will be used.
  • cacheSegment (Default: '@fastify/caching'): segment identifier to use when communicating with the cache.
  • serverExpiresIn (Default: undefined): a value, in seconds, for the length of time the resource is fresh and may be held in a shared cache (e.g. a CDN). Shared caches will ignore max-age when this is specified, though browsers will continue to use max-age. Should be used with expiresIn, not in place of it. When this is set, and privacy is set to public, then ', s-maxage=<value>' will be appended to the cache-control header.

reply.etag(string, number)

This method allows setting of the etag header. It accepts any arbitrary string. Be sure to supply a string that is valid for HTTP headers.

If a tag string is not supplied then uid-safe will be used to generate a tag. This operation will be performed synchronously. It is recommended to always supply a value to this method to avoid this operation.

All incoming requests to paths affected by this plugin will be inspected for the if-none-match header. If the header is present, the value will be used to lookup the tag within the cache associated with this plugin. If the tag is found, then the response will be ended with a 304 status code sent to the client.

Tags will be cached according to the second parameter. If the second parameter is supplied, and it is an integer, then it will be used as the time to cache the etag for generating 304 responses. The time must be specified in milliseconds. The default lifetime, when the parameter is not specified, is 3600000.

reply.expires(date)

This method allows setting of the expires header. It accepts a regular Date object, or a string that is a valid date string according to RFC 2616 §14.21.

License

MIT License

fastify-caching's People

Contributors

anthonyringoet avatar bertday avatar cemremengu avatar darkgl0w avatar delvedor avatar dependabot-preview[bot] avatar dependabot[bot] avatar eomm avatar fdawgs avatar frikille avatar genediazjr avatar greenkeeper[bot] avatar jorisdugue avatar jsumners avatar juliegoldberg avatar lependu avatar mcollina avatar metcoder95 avatar nimasrn avatar prevostc avatar salmanm avatar serayaeryn avatar tiswrt avatar uzlopak avatar zekth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fastify-caching's Issues

support async/await out of the box

The idea is that we should not pass useAwait but rather return a Promise if there is not callback.

const redis = require('ioredis')({host: '127.0.0.1'})
const abcache = require('abstract-cache')({
  driver: {
    name: 'abstract-cache-redis', // must be installed via `npm install`
    options: {client: redis}
  }
})

const fastify = require('fastify')()
fastify
  .register(require('fastify-redis'), {client: redis})
  .register(require('fastify-caching'), {cache: abcache})

fastify.get('/', (req, reply) => {
  await fastify.cache.set('hello', {hello: 'world'}, 10000
  return {hello: 'world'}
})

fastify.listen(3000, (err) => {
  if (err) throw err
})

old etags (aka etag removal)

Etags are added to the cache as they are seen, but what happens if the server updates a page and its etag (after a db change, whatever). Are requests including the previous etag still returning 304? Do we have to manage that manually, thru the abstract-cache interface?

Is it possible to set max-age?

I would like to know how to set max-age for a specific path.
The logo in this example.

module.exports = function(fastify, opts, next) {
  fastify.register(require('fastify-caching'))
  fastify.get('/logo.png', handler.logo)
  fastify.get('/nocache.js', handler.js)
}

module.exports = {
  logo: function(request, reply) {
    const stream = fs.createReadStream(
      path.join(appRoot.path, '/build/assets/logo.png'),
      'utf8'
    )
    reply
      .header('Content-Type', 'image/png')
      .send(stream)
  }
  nocache: function(request, reply) {
    const stream = fs.createReadStream(
      path.join(appRoot.path, '/build/assets/nocache.js'),
      'utf8'
    )
    reply
      .header('Content-Type', 'application/javascript; charset=UTF-8')
      .send(stream)
  }
}

Add typescript definitions

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

🚀 Feature Proposal

Add typescript definitions for use in typescript projects

Motivation

I'm currently using fastify-caching in a typescript project, however there are no type definitions so typescript goes in error.
Note that the abstract-cache library suffers from the same issue.

Example

import fastifyCaching from "fastify-caching"
Could not find a declaration file for module 'fastify-caching'. '/[**REDACTED**]/node_modules/fastify-caching/plugin.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/fastify-caching` if it exists or add a new declaration (.d.ts) file containing `declare module 'fastify-caching';`ts(7016)

Cache object is empty

Hello guys,

unfortunately I am not able to implement this plugin successfully. The cache instance remains empty when I try to add data to it.

app.js

const redis = require('ioredis')({ host: '127.0.0.1' })
const abcache = require('abstract-cache')({
  useAwait: false,
  driver: {
    name: 'abstract-cache-redis',
    options: { client: redis }
  }
})
fastify.register(require('fastify-redis'), { client: redis })
fastify.register(require('fastify-caching'), { cache: abcache })

route.js

module.exports = function (fastify, opts, done) {
    fastify.get('/test', test)
    done()
}

controller.js

exports.test = function (req, res) {
	this.cache.set("test", {hello: "world"}, 10000, () => {
		console.log(this.cache)
		res.send(this.cache.get("test"))
	})
}

What's the problem here? I appreciate your help.

Best

cache::get returns void in typescript

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.23.2

Plugin version

8.3.0

Node.js version

18.0.0

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

22.04

Description

When using local cache (e.g. fastify.cache.set and fastify.cache.get), fastify.cache.get returns void by typescript definition.

Steps to Reproduce

Use typescript and run const res = await fastify.cache.get('something')

res is void according to typescript and will result in compile error.

Expected Behavior

No response

Evaluate for release

@fastify/fastify please review this module. Give me a 👍 if you think it is ready to be released.

Delete Method

I had trouble with

fastify.cache.delete(key);

But now it's ok. Sorry. My misatke.

Incoming "last-modified" requests

Clients, upon receiving resources with a Last-Modified header, may send requests that contain one or both of the above linked headers. I think handling these headers would be outside the scope of this plugin. At best, I think the incoming request object could be decorated with the values of these headers, e.g.:

fastify.decorateRequest('ifModifiedSince', function () {
  if (!this.req.headers['if-modified-size']) return null
  return new Date(this.req.headers['if-modified-since'])
})

Do you agree @fastify?

Support Dragonfly

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

🚀 Feature Proposal

Support Dragonfly as a drop-in replacement for Redis. Moreover, Dragonfly is also much faster than Redis!

Motivation

Now Redis is adopting dual license, people will move away from Redis quickly to alternatives. Like Dragonfly.

More info: https://redis.com/blog/redis-adopts-dual-source-available-licensing/

Example

No response

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.

Since we didn’t receive a CI status on the greenkeeper/initial branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/.

Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper App’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Caching object

@fastify please look at commit 7b72da2 and give me your feedback. It adds a decorator that provides an object to facilitate server-local caching of items. Basically, anything that conforms to the Catbox v7 API can be supplied as a cache object. The default is a very simple memory cache.

Private cache control

It would be useful to implement a helper function to indicate that the current request should not be stored by shared cache.

cache-control: max-age=31536000, must-revalidate, private
reply
    .lastModified(new Date())
    .private()
    .send({hello: 'world'})

This functionality is provided by many http frameworks which provide auth ouf of the box.

Fatal effect @fastify/static

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.19.2

Plugin version

4.5.0

Node.js version

20.4

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

Linux Mint 21.1 Cinnamon

Description

After @fastify/caching is activated (by uncommenting the first 4 lines) the index.html loading continues and never terminates successfully. Tia

Steps to Reproduce

app.js

import Fastify from 'fastify'
// import fastifyCaching from '@fastify/caching'
import fastifyStatic from '@fastify/static'
// import abstractCache from 'abstract-cache'

const fastify = Fastify()
// const abCache = abstractCache({ useAwait: true })

// await fastify.register(fastifyCaching, {cache: abCache})
await fastify.register(fastifyStatic)

fastify.get('/json', {}, async (request, reply) => {
  reply.type('application/json').code(200)
  return { hello: 'world' }
})

fastify.get('/html', {}, async (request, reply) => {
  return reply.sendFile('index.html')
})

const start = async () => {
  try {
    await fastify.listen({port: 8000, host: 'localhost'})
  } catch (err) {
    // fastify.log.error(err)
    console.log('Found error: ', err)
    process.exit(1)
  }
}
start()

index.html

<html>
  <body>
    <b>Welcome!</b>
  </body>
</html>

Expected Behavior

fastifyCaching should not interfere with fastifyStatic so that index.html to be successfully loaded.

Abstract-cache with useAwait:true, and an etag on reply, causes responses to hang

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure it has not already been reported

Fastify version

3.20.2

Plugin version

6.1.0

Node.js version

14.147.5

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

11.4

Description

Using fastify-cache with abstract-cache({ useAwait: true }) and setting an etag on reply will cause the response to hang if/when the browser sends back an etag in the if-none-match header.

This manfests as initial responses in browser returning as expected (because if-none-match is not sent), but any subsequent request will hang.

Same issue also with etagOnSend.

Steps to Reproduce

In plugin.js... you can see that the callback passed to this.cache.get will never be called, if useAwait is true on abstract-cache.

function etagHandleRequest (req, res, next) {
  if (!req.headers['if-none-match']) return next()
  const etag = req.headers['if-none-match']
  this.cache.get({ id: etag, segment: this.cacheSegment }, (err, cached) => {
    if (err) return next(err)
    if (cached && cached.item) {
      return res.status(304).send()
    }
    next()
  })
}

Expected Behavior

If useAwait is true, the etagHandleRequest response callback is executed after the abstract-cache.get promise resolves or rejects.

This could potentially also be resolved by allowing callbacks to optionally be passed and executed on promise resolution here: https://github.com/jsumners/abstract-cache/blob/master/lib/wrapCB.js...

TypeError: fastify.register error in sample usage in docs

🐛 Bug Report

the sample usage has some error in using and import Fastify
When I use the example code and run the node server I get the error TypeError: fastify.register is not a function

To Reproduce

const http = require('http')
const fastify = require('fastify')
const fastifyCaching = require('fastify-caching')

fastify.register(
  fastifyCaching,
  {privacy: fastifyCaching.privacy.NOCACHE},
  (err) => { if (err) throw err }
)

fastify.get('/', (req, reply) => {
  reply.send({hello: 'world'})
})

fastify.listen(3000, (err) => {
  if (err) throw err

  http.get('http://127.0.0.1:3000/', (res) => {
    console.log(res.headers['cache-control'])
  })
})

Expected behavior

not getting the TypeError and server runs successfully

Your Environment

  • node version: 14
  • fastify version: >=3.0.0
  • os: Linux

Action required: Greenkeeper could not be activated 🚨

🚨 You need to enable Continuous Integration on all branches of this repository. 🚨

To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because we are using your CI build statuses to figure out when to notify you about breaking changes.

Since we did not receive a CI status on the greenkeeper/initial branch, we assume that you still need to configure it.

If you have already set up a CI for this repository, you might need to check your configuration. Make sure it will run on all new branches. If you don’t want it to run on every branch, you can whitelist branches starting with greenkeeper/.

We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.

Once you have installed CI on this repository, you’ll need to re-trigger Greenkeeper’s initial Pull Request. To do this, please delete the greenkeeper/initial branch in this repository, and then remove and re-add this repository to the Greenkeeper integration’s white list on Github. You'll find this list on your repo or organization’s settings page, under Installed GitHub Apps.

Default cache not working

I am using fastify-compress with fastify-static, and every request is being recompressed before sending to the client.

Is this plugin meant to cache the compressed results of previous requests and serve those instead of going to disk again?

Error when trying to use reply.etag and Redis

Hi,

I'm trying to build a POC where I can use both functionalities from fastify-caching:

  • Being able to set the object in a local Redis I have
  • Add etag header using reply.etag

This is what I have in my plugin:

/* eslint-disable no-console */
const fp = require("fastify-plugin");

module.exports = fp(function(fastify, opts, next) {
  const Redis = require("ioredis");

  const redis = new Redis({ host: "127.0.0.1", port: 6379 });

  const abcache = require("abstract-cache")({
    useAwait: false,
    driver: {
      name: "abstract-cache-redis",
      options: { client: redis }
    }
  });

  fastify.register(require("fastify-redis"), { client: redis });
  fastify.register(require("fastify-caching"), { cache: abcache });

  next();
});

And this is my request handler:

/* eslint-disable no-console */
module.exports = async fastify => {
  fastify.get("/", (req, reply) => {
    fastify.cache.set("hello", { hello: "world" }, 10000, err => {
      if (err) return reply.send(err);
      reply.etag("hey").send({ hello: "world" });
    });
  });
};

What I'm getting when I do a request is the following:

13:08:56.423] ERROR (48606 on xxxx-MacBook-Pro.local): Attempted to send payload of invalid type 'number'. Expected a string or Buffer.
    reqId: 1
    req: {
      "id": 1,
      "method": "GET",
      "url": "/hello",
      "hostname": "127.0.0.1:4444",
      "remoteAddress": "127.0.0.1",
      "remotePort": 61836
    }
    res: {
      "statusCode": 500
    }
    err: {
      "type": "TypeError",
      "message": "Attempted to send payload of invalid type 'number'. Expected a string or Buffer.",
      "stack":
          TypeError: Attempted to send payload of invalid type 'number'. Expected a string or Buffer.
              at onSendEnd (/Users/devgurus/projects/tottus/fastify-soler/node_modules/fastify/lib/reply.js:207:11)
              at wrapOnSendEnd (/Users/devgurus/projects/tottus/fastify-soler/node_modules/fastify/lib/reply.js:177:5)
              at next (/Users/devgurus/projects/tottus/fastify-soler/node_modules/fastify/lib/hookRunner.js:43:7)
              at client.set.then (/Users/devgurus/projects/tottus/fastify-soler/node_modules/abstract-cache/lib/wrapAwait.js:24:25)
              at process._tickCallback (internal/process/next_tick.js:68:7)
    }

If I remove the etag("hey"), the request is handled perfectly and I get the response as well as the object is added to the Redis cache.

Am I configuring something wrong?


Node: v10.13.0
fastify: ^1.13.0
fastify-caching: ^3.0.0
fastify-redis: ^2.0.0
abstract-cache: ^1.0.1
abstract-cache-redis: ^1.0.2

Static files can't be loaded with useAwait caching

Hello guys,

since I work with Javascript since 2 months only it's not that easy to build my app. I recently integrated this plugin as mentioned in the readme.

app.js

const redis = require('ioredis')({ host: '127.0.0.1' })
const abcache = require('abstract-cache')({
  useAwait: true,
  driver: {
    name: 'abstract-cache-redis',
    options: { client: redis }
  }
})
fastify.register(require('fastify-redis'), { client: redis })
const fastifyCaching = require('fastify-caching')
fastify.register(fastifyCaching, {
  cache: abcache
})

fastify.register(require('point-of-view'), {
  engine: {
    pug: require('pug')
  },
  templates: path.join(__dirname, './', 'views')
})

fastify.register(require('fastify-static'), {
  root: path.join(__dirname, 'public')
})

I activated "useAwait", but that's the whole problem right now. If useAwait is false everything works fine. If useAwait is true the page loads only if I request the url twice. This is related to the css, js and media files from my static folder. If I remove all static files or if I disable cache in my browser the page will be loaded normally.

The files are on pending:
Screenshot

Any ideas how to fix this?

Best

Set Etag automatically for each request

We can provide etag parameter in initial config which is by default set to true. The behaviour will be to automatically set etag header on all successful requests. In fact current implementation is not standards compliant. If I do reply.etag(), it always generates new etag even for same payload. Ideally it should generate same for same payload.

Something like this should be good.

let etag = crypto
    .createHash('sha1')
    .update(payload)
    .digest('base64')
    .substring(0, 27);
  reply.etag(etag);

Create 6.3 release with new type definitions

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the issue has not already been raised

Issue

Hey there, thank you for all of your hard work on this library!

I noticed that #90 was recently merged to the main branch, but there hasn't been a release since 6.2 in November 2021. Is it possible to cut another release with these TS additions?

Is there any way to set the cache segment dynamically?

Use Case: I want to have multiple cache segments, one for users, and for example one for precipitation probabilities by region. How would I go about having two cache segments?

My ideal situation would be if I could set the segment when calling get/set..

Example:

...
fastify.register(FastifyCache, { privacy: "private", expiresIn: 1000 * 60 * 60 });
...
let user;
user = fastify.cache.kv.get(req.user.sub);
if (!user) {
   const userModel = await Users.findOrCreate({
      where: { key: req.user.sub }
   });
   fastify.cache.kv.set(req.user.sub, userModel[0]); // 1hr default
   user = fastify.cache.kv.get(req.user.sub);
}
req.auth = user;
let location = location;
let probabilityPrecip = fastify.cache.kv.get(location);
if (!probabilityPrecip){
   const newProbabilityPrecip = await Precipitation.get(location);
   fastify.cache.kv.set(location, newProbabilityPrecip); // 1hr default
   probabilityPrecip = fastify.cache.kv.get(location);
}
req.prob = probabilityPrecip

In this example - I would ideally have a separate segment for each store... namely:
fastify.cache.kv.get(user)
fastify.cache.kv.get(uset, { cacheSegment: 'users' })
fastify.cache.kv.get(location)
fastify.cache.kv.get(location, { cacheSegment: 'locations' })

I tried this and it doesn't seem to be possible given the signatures of get / set

set:
set (key,val,ttl=0,cb=function () {...
get:
get(key){...

Any ideas how to accomplish this? I don't think I can register a second cache in fastify since the fastify.cache namespace is hardcoded in the lib

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.