Cachet: An open-source module for robust caching

Cachet was made due to personal fustration with repeated code responsible for caching. I designed it to be as powerful and robust as possible to be a one-fits-all solution for my caching needs.

Cachet ended up being a really nice module, so I decided to make it open-source.

Code Examples

Hello, Cachet!

This is a very basic example showing how simple it is to get set-up with Cachet.

-- Import Cachet
local Cachet = require(path.to.cachet)
-- Create a new cache, setting 'favouriteNumber' to 32 right off the bat
local cache = Cachet.new({ favouriteNumber = 32 })
-- Output the current value of 'favouriteNumber' (32)
print(cache:retrieve("favouriteNumber"))

-- Connect a callback to when the cache is updated
-- and output details about the change.
cache:connect(function(...)
	print(...)
end)

-- Update 'favouriteNumber' to 64 and invalidate this entry afte 10 seconds
-- This means that after 10 seconds, 'favouriteNumber' will be invalidated
-- and set to nil.
cache:store("favouriteNumber", 64, 10)

-- Output the current value of favouriteNumber (now 64)
print(cache:retrieve("favouriteNumber"))

Using this code, we should get the following output immediately:

  32 table: 0x6bf449a775e95c23
  favouriteNumber 32 64 12951698-3505-4A42-B704-8742B74462E6
  64 table: 0xe33151b518f7cd33

And then ten seconds later:

  favouriteNumber 64 nil nil

This shows:

  1. Where we define our original favouriteNumber to 32.
  2. It then shows where we change our favouriteNumber to 64.
  3. And finally, it shows where the cache is invalidated ten seconds later

As you can see, it is very easy to get started with Cachet.

Cooldown System

Cachet is designed to be a very powerful system and can be used in very specific use cases.
In this example, we’ll create a basic (but inefficient) command-cooldown system using Cachet’s features

-- Import Cachet
local Cachet = require(workspace.Cachet)
-- Create a new cache. We don't need any default values yet
local cooldowns = Cachet.new()

local commandCooldowns = {
	kill = 5, -- Give the 'kill' command a 5 second cooldown
	kick = 20, -- Give the 'kick' command a 20 second cooldown
}

local function checkCooldown(player, command)
	-- Check the cooldown. Note that keys must be strings.
	if cooldowns:retrieve(tostring(player.UserId)) then
		-- The user is within the cooldown. This command should not run.
		return false
	end
	
	-- Update the cooldown based on the command
	cooldowns:store(tostring(player.UserId), true, commandCooldowns[command])
	return true
	
	--[[
		While this is not an ideal system, it still is a pretty good example.
		Additionally, you could instead cache timestamps (or use the cache entrys's 'stored' meta-property).
	]]
end

checkCooldown(player1, "kick") --> true
checkCooldown(player1, "kick") --> false
wait(20)
checkCooldown(player1, "kick") --> true
checkCooldown(player1, "kill") --> false

If you have an example you’d like to show off here, DM me on the DevForum with it!


Documentation & Guide

All relevant information can be found on the README. In the future, I’ll probably set up a handy website containing all details and guides, instead of a basic API reference.


Changelog

I will be posting news, changelogs, help requests, etc on this thread. Please mark it as “watching” so you get notifications whenever I post on it.

Inspired by DataStore2

Please do not hesitate to ask if you have any questions or concerns! <3

26 Likes

Thanks, this is an amazing module. However, do you recommend using this for a game with large traffic?

1 Like

Cachet makes no web requests. Internally, it stores cached data in a dictionary.
This means it would be as fast as a regular dictionary storing cache values. Cachet just stores additional data about things such as timestamps and has helper functions.

If you see a part of the code that can be made more efficient, please please please please open an issue or pull request!!

The cooldown looks very cool. I can’t wait to give it a try.

1 Like

Going to implement this :slight_smile:
I’ll let you know how it goes.

1 Like

I feel like this is a rather inefficient method of invalidating keys and keeps an unnecessary thread hanging around. This could be really expensive if someone were to store a lot of cached keys assuming they use the cache correctly and always expire keys. Although I don’t believe someone could reach limits in a single server but if it was shared among servers then quite possibly. Maybe when universe scripts come and all servers can store central data somewhere this could be a handy module.

if invalidateAfter then
	coroutine.wrap(function()
		wait(invalidateAfter)
		self:invalidateGUID(guid)
	end)()
end

Take a note from Redis, a popular memory database, where it doesn’t actually do this and instead checks the expiration when the key is read as well as periodically polling random keys that are in an expiration set and removing them every few seconds. I recommend doing some research on how Redis operates as it could be beneficial.

1 Like

I’ve opened an issue about this.

Do you have any example use cases for this? It sounds like it could be useful, but I don’t know what for.

1 Like

In game programming, you will need to cache information (mostly information that you fetch from API calls, e.g. group ranks). Cachet was built to make this caching much easier, more powerful, and more efficient.

You can also think outside the box and use it in other cases (e.g. command cooldowns, as you can see in one of the code examples).

1 Like