Readme

Implementation of Redis-like cache - a key-value store with expiring keys. Data is stored in the Val Town SQLite database and shared between all your vals.

Setup

  1. First you should decide on a name of a SQL table that will be used for storing cache data. It could something like cacheData or kv. Set that value to a new Environment Variable CACHE_TABLE_NAME.
  2. Optionally you might add a new CACHE_DEFAULT_TTL Environment Variable. It's value should be set to a number of seconds that will be used when saving new values to the cache without providing the expiration time. By default it's 24h.
  3. The setup() function should be ran before using the cache for the first time. You can do that by creating a small temporary Val:
    import { setup } from "https://esm.town/v/xkonti/cache"; await setup();
  4. Optionally create a scheduled val that will delete expired keys on some interval - 15 minutes can be a good start.
    import { deleteExpired } from "https://esm.town/v/xkonti/cache"; export default async function cacheCleaner(interval: Interval) { await deleteExpired(); }

Usage

After setting your cache up you can use it simply by importing functions from https://esm.town/v/xkonti/cache.

set(key, value, ttl): Promise

Set a value in the cache.

  • key - the key to set the value for
  • value - the value to set - it can be any value that can be serialized to JSON
  • ttl - the time to live in seconds. In other words, after how many seconds the key will expire. If not set, the default TTL is used.
  • returns the number of keys set: 1 if the key was inserted/updated, 0 if the ttl was 0 or invalid
// Set a value in the cache with a default TTL await set("luckyNumber", 13); // Set a value that will expire in 5 minutes await set("product:344798", { name: "Audio Interface", price: 209.99}, 5 * 60);

setUntil(key, value, expiresAt): Promise

Set a value in the cache until a specific date and time.

  • key - the key to set the value for
  • value - the value to set - it can be any value that can be serialized to JSON
  • expiresAt - the expiration time as a UTC date string
  • returns the number of keys set: 1 if the key was inserted/updated, 0 if the `expiresAt`` was in the past
// Set a value in the cache until 2024-01-01 16:23:05 UTC await setUntil( "product:155392", { name: "Audio Interface", price: 209.99 }, new Date('2024-01-01T16:23:05Z').toISOString() );

setExpiration(key, ttl): Promise

Update the expiration date of a cache entry based on TTL. If the key does not exist or is expired, nothing happens.

  • key - the key of the cache entry to update
  • ttl - the time to live in seconds from now. In other words, after how many seconds the key will expire. If not set, the default TTL is used.
  • returns the number of keys updated: 1 if updated, 0 if not found or ttl was 0
// Set the expiration date in the cache with a default TTL await setExpiration("luckyNumber"); // Set the expiration date in the cache for 5 minutes from now. await setExpiration("luckyNumber", 5 * 60);

setExpirationUntil(key, expiresAt): Promise

Update the expiration date of a cache entry to a specific UTC date and time. If the key does not exist or is expired, nothing happens.

  • key - the key of the cache entry to update
  • expiresAt - the expiration time as a UTC date string
  • returns the number of keys updated: 1 if updated, 0 if not found or expiresAt was in the past
// Set the expiration date in the cache until 2024-01-01 16:23:05 UTC await setExpirationUntil( "product:155392", new Date('2024-01-01T16:23:05Z').toISOString() );

exists(key): Promise

Checks if the provided key exists (has value) in the cache. If the key is expired, it's considered non-existent.

  • key - the key to check for existence
// Check if the key is present in the cache const hasLuckyNumber: Boolean = await exists("luckyNumber");

get(key): Promise<T | null>

Get a value from the cache by key. You can provide a type of the return value or it will default to unknown. If there is no value for the key or the value has expired, null is returned.

  • key - the key to get the value for
// Get a value from the cache const luckyNumber: number = await get<number>("luckyNumber"); const luckyNumber: number = await get("luckyNumber") as number; // same as above

listKeys(prefix): Promise<string[]>

Gets a list of all non-expired keys in the cache that match the prefix. If no prefix is provided, all keys are returned.

  • prefix - the optional prefix to match keys against
// Get all keys from the cache const keys: string[] = await listKeys(); // Get all keys from the cache that start with "product:" const keys: string[] = await listKeys("product:");

getMany(prefix, limit): Promise<Array<{ key: string, value: T }>>

Get many key-value pairs from the cache that match the given prefix.

  • prefix - the optional prefix to match keys against. If not provided, all keys are considered.
  • limit - the optional maximum number of key-value pairs to return. If 0, no limit is applied. Defaults to 0.
  • returns An array of key-value pairs. Each pair is an object with key and value properties.
// Get all non-expired keys and their values const everything = await getMany(); // Get all keys and values with a matching prefix const allProducts = await getMany("product:"); // Get 5 keys and values with a matching prefix const discountedProducts = await getMany("discounts:", 5);

deleteKey(key): Promise

Delete a key from the cache.

  • key - the key to delete
  • returns the number of keys deleted: 1 if the key was deleted, 0 if the key did not exist.
// Delete a key from the cache await deleteKey("luckyNumber");

deleteKeys(prefix): Promise

Delete all keys from the cache that match the prefix. If no prefix is provided, all keys in the cache are deleted.

  • prefix - the optional prefix to match keys against
  • returns the number of keys deleted
// Delete all keys from the cache await deleteKeys(); // Delete all keys from the cache that start with "product:" await deleteKeys("product:");

deleteExpired(): Promise

Delete all expired keys from the cache. Perfect for running on a schedule to keep the cache small and fast.

  • returns the number of keys deleted
// Delete all expired keys from the cache await deleteExpired();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { sqlite } from "https://esm.town/v/std/sqlite";
// Retrieve the table name from the environment variable
// Fail if it's not present.
const tableName = Deno.env.get("CACHE_TABLE_NAME");
if (tableName === undefined) {
console.error(
"Env variable 'CACHE_TABLE_NAME' is not configured. Stopping execution.",
);
throw new Error("Env variable 'CACHE_TABLE_NAME' is not configured");
}
/**
* Defualt TTL (time to live) - this describes how long a default cache entry
* is guaranteed to persist. The default value is 24 hours, can be changed by
* setting the `CACHE_DEFAULT_TTL` environment variable with a new default value
* in seconds.
*/
const defaultTTL = parseInt(
Deno.env.get("CACHE_DEFAULT_TTL") || `${24 * 60 * 60}`,
);
/**
* Setup the cache table if it does not exist.
* Make sure to set the `CACHE_TABLE_NAME` environment variable first.
*/
export async function setup() {
await sqlite.execute(`
CREATE TABLE IF NOT EXISTS ${tableName} (
key TEXT PRIMARY KEY,
content TEXT NOT NULL,
expires_at DATETIME
)
`);
console.log("Cache setup complete.");
}
/**
* Check if the provided key exists (has value) in the cache.
* If the key is expired, it's considered non-existent.
* @param key The key to check if exists
* @returns The value indicating whether the key is present in cache.
*/
export async function exists(key): Promise<Boolean> {
const result = await sqlite.execute({
sql: `SELECT 1 FROM ${tableName} WHERE key = :key AND (expires_at IS NULL OR expires_at > datetime('now'))`,
args: { key },
});
return result.rows.length > 0;
}
/**
* Get a value from the cache by key. You can provide a type of the return value or it will default to `unknown`.
* @param key The key to get the value for
* @returns The value for the key, or `null` if the key does not exist or has expired
*/
export async function get<T = unknown>(key): Promise<T | null> {
const result = await sqlite.execute({
sql: `SELECT content FROM ${tableName} WHERE key = :key AND (expires_at IS NULL OR expires_at > datetime('now'))`,
args: { key },
});
return result.rows.length > 0
? JSON.parse(result.rows[0][0] as string) as T
: null;
}
/**
* Set a value in the cache
* @param key The key to set the value for
* @param value The value to set
* @param ttl The time to live in seconds. If not set, the default TTL is used.
* @returns The number of keys set (1 if the key was inserted/updated, 0 if the ttl was 0)
*/
export async function set(key, value, ttl: number = defaultTTL): Promise<number> {
if (ttl <= 0) return 0;
const expires_at = ttl ? `datetime('now', '+${ttl} seconds')` : null;
const result = await sqlite.execute({
sql:
`INSERT INTO ${tableName} (key, content, expires_at) VALUES (:key, :content, ${expires_at}) ON CONFLICT(key) DO UPDATE SET content = :content, expires_at = ${expires_at}`,
args: { key, content: JSON.stringify(value) },
});
return result.rowsAffected;
}
/**
* Set a value in the cache
* @param key The key to set the value for
* @param value The value to set
* @param expiresAt The expiration time as a UTC date string. If not set, the default expiration is used.
* @returns The number of keys set (1 if the key was inserted/updated, 0 if the expiresAt was in the past)
*/
export async function setUntil(key: string, value: unknown, expiresAt: string): Promise<number> {
const currentDateTime = new Date().toISOString();
if (expiresAt <= currentDateTime) return 0;
const result = await sqlite.execute({
sql:
`INSERT INTO ${tableName} (key, content, expires_at) VALUES (:key, :content, :expiresAt) ON CONFLICT(key) DO UPDATE SET content = :content, expires_at = :expiresAt`,
args: { key, content: JSON.stringify(value), expiresAt },
});
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
2
xkonti avatar

Changes in version 18

Added exists(key): Promise<Boolean> Checks if the provided key exists (has value) in the cache. If the key is expired, it's considered non-existent.

// Check if the key is present in the cache const hasLuckyNumber: Boolean = await exists("luckyNumber");
pomdtr avatar

small feedback about the CACHE_TABLE_NAME setup steps.

I feel like it would a better pattern to initialize your app by creating a val with the following content

import { createCache } from "https://esm.town/v/xkonti/cache?v=18" // all the function are exported here export const cache = createCache({ tableName: "my-table-name" })

The main benefit is that it allows me to create as much cache as I want, and allow me to easily upgrade to a new version by just changing the query param.

June 1, 2024