Skip to main content

Cache usage

info

Please note Cache module

Initial configuration

To begin using the Cache class, you'll need to create and configure an instance:

import { TimeSpan } from "@daiso-tech/core/utilities";
import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { Namespace } from "@daiso-tech/core/utilities";

const cache = new Cache({
// You can provide default TTL value
// If you set it to null it means keys will be stored forever.
defaultTtl: TimeSpan.fromSeconds(2),

namespace: new Namespace("cache"),

// You can choose the adapter to use
adapter: new MemoryCacheAdapter(),
});
info

Here is a complete list of configuration settings for the Cache class.

Cache basics

Adding keys

You can add a key and provide a optional TTL to overide the default:

await cache.add("a", "value", TimeSpan.fromSeconds("1"));
danger

Note cache class instance uses LazyPromise instead of a regular Promise. This means you must either await the LazyPromise or call its defer method to run it. Refer to the LazyPromise documentation for further information.

Retrieving keys

You can retrieve the key:

await cache.get("a");

Checking key existence

You can check if the key exists:

await cache.exists("a");

You can check if the key is missing:

await cache.missing("a");

Updating keys

You can update a key value and true will be returned if the key exists and was updated:

await cache.update("a", 2);

You can increment the a key and true will be returned if the key exists and was updated. If the key is not a number an error will be thrown:

await cache.increment("a", 2);

You can decrement the a key and true will be returned if the key exists and was updated. If the key is not a number an error will be thrown,:

await cache.decrement("a", 1);

You can replace the key value with a given TTL if the key exists otherwise the key will be added:

await cache.put("a", 2);
await cache.put("a", 2, TimeSpan.fromSeconds(3));

Removing keys

You can remove a key and true will be returned if the key was found and removed:

await cache.remove("a");

You can remove multiple keys and true will be returned if one of the keys exists and where removed:

await cache.removeMany(["a", "b"]);

You can clear all the keys of the given namespace:

await cache.clear();

Patterns

Iterable as key

You can use an Iterable<string> as a key. The elements will be joined into a single string, and the delimiter used for joining is configurable in the Namespace class:

const cache = new cache({
namespace: new Namespace("lock"),
// rest of the settings ....
});

await cache.add(["user", "1"]);

await cache.removeMany([
["user", "1"],
["user", "1"],
]);
info

Note this works also with following methods, exists, missing, get, getOrFail, getAndRemove, getOr, getOrAdd, put, update, increment, decrement and remove.

Compile time type safety

You can enforce compile time type safety by setting the cache value type:

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { Namespace } from "@daiso-tech/core/utilities";

type IUser = {
name: string;
email: string;
age: number;
};

const cache = new Cache<IUser>({
namespace: new Namespace("cache"),
adapter: new MemoryCacheAdapter(),
});

// A typescript error will occur because the type is not mathcing.
await cache.add("a", "asd")

If you have multiple types you can use algeberical enums:

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { Namespace } from "@daiso-tech/core/utilities";

type IUser = {
type: "USER";
name: string;
email: string;
age: number;
};
type IProduct = {
type: "PRODUCT";
name: string;
price: number;
};
type CacheValue = IUser | IProduct;

const cache = new Cache<CacheValue>({
namespace: new Namespace("cache"),
adapter: new MemoryCacheAdapter(),
});

const cacheValue = await cache.get("user1");
// You need to check the type is "USER" inorder to access IUser fields.
if (cacheValue.type === "USER") {
console.log(cacheValue.name, cacheValue.age);
}
// You need to check the type is "PRODUCT" inorder to access IProduct fields.
if (cacheValue.type === "PRODUCT") {
console.log(cacheValue.name, cacheValue.price);
}

Alternatively you can use different Cache classes with different namespaces:

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { Namespace } from "@daiso-tech/core/utilities";

const cacheAdapter = new MemoryCacheAdapter();

type IUser = {
name: string;
email: string;
age: number;
};
const userCache = new Cache<IUser>({
namespace: new Namespace(["cache", "user"]),
adapter: cacheAdapter,
});

type IProduct = {
name: string;
price: number;
};
const productCache = new Cache<IProduct>({
namespace: new Namespace(["cache", "product"]),
adapter: cacheAdapter,
});

Runtime type safety

You can enforce runtime and compiletime type safety by passing standard schema to the cache:

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { z } from "zod";

const userSchema = z.object({
name: z.string();
email: z.string();
age: z.number();
});

// The type will be infered
const cache = new Cache({
adapter: new MemoryCacheAdapter(),
schema: userSchema
});

// A typescript and runtime error will occur because the type is not mathcing.
await cache.add("a", "asd")

Additional methods

You can retrieve the key and if it does not exist an error will be thrown:

await cache.getOrFail("ab");

You can retrieve the key and if it does not exist you can return a default value:

await cache.getOr("ab", 1);

You can retrieve the key and if it does not exist you can insert a default value that will aslo be returned:

await cache.getOrAdd("ab", 1);
info

You can provide LazyPromise, synchronous and asynchronous Invokable as default values for both getOr and getOrAdd methods.

You can retrieve the key and afterwards remove it and will return true if the value was found:

await cache.getAndRemove("ab");

Cache events

You can listen to different cache events (CacheEventMap) that are triggered by the Cache. Refer to the EventBus documentation to learn how to use events.

import { CACHE_EVENTS } from "@daiso-tech/core/cache/contracts";

// Will log whenever an item is added, updated and removed
await cache.subscribe(CACHE_EVENTS.WRITTEN, (event) => {
console.log(event);
});

await cache.add("a", "b");
await cache.update("a", 1);
await cache.increment("a", 1);
await cache.remove("a");
info

Note the Cache class uses MemoryEventBusAdapter by default. You can choose what event bus adapter to use:

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { RedisPubSubEventBus } from "@daiso-tech/core/event-bus/adapters";
import { EventBus } from "@daiso-tech/core/event-bus";
import { Namespace } from "@daiso-tech/core/utilities";
import { RedisPubSubEventBusAdapter } from "@daiso-tech/core/event-bus/adapters";
import { Serde } from "@daiso-tech/core/serde";
import { SuperJsonSerdeAdapter } from "@daiso-tech/core/serde/adapters";
import Redis from "ioredis";

const serde = new Serde(new SuperJsonSerdeAdapter());

const redisPubSubEventBusAdapter = new RedisPubSubEventBusAdapter({
dispatcherClient: new Redis("YOUR_REDIS_CONNECTION_STRING"),
listenerClient: new Redis("YOUR_REDIS_CONNECTION_STRING"),
serde,
});

const cache = new Cache({
namespace: new Namespace("cache"),
adapter: new MemoryCacheAdapter(),
eventBus: new EventBus({
namespace: new Namespace("event-bus"),
adapter: redisPubSubEventBusAdapter,
}),
});
info

Note you can disable dispatching Cache events by passing an EventBus that uses NoOpEventBusAdapter

warning

If multiple cache adapters (e.g., RedisCacheAdapter and MemoryCacheAdapter) are used at the same time, isolate their events by assigning separate namespaces. This prevents listeners from unintentionally capturing events across adapters.

import { MemoryCacheAdapter } from "@daiso-tech/core/cache/adapters";
import { Cache } from "@daiso-tech/core/cache";
import { EventBus } from "@daiso-tech/core/event-bus";
import { Namespace } from "@daiso-tech/core/utilities";
import { RedisPubSubEventBusAdapter } from "@daiso-tech/core/event-bus/adapters";
import { Serde } from "@daiso-tech/core/serde";
import { SuperJsonSerdeAdapter } from "@daiso-tech/core/serde/adapters";
import Redis from "ioredis";

const serde = new Serde(new SuperJsonSerdeAdapter());

const redisPubSubEventBusAdapter = new RedisPubSubEventBusAdapter({
dispatcherClient: new Redis("YOUR_REDIS_CONNECTION_STRING"),
listenerClient: new Redis("YOUR_REDIS_CONNECTION_STRING"),
serde,
});

const memoryCacheAdapter = new MemoryCacheAdapter();
const memoryCache = new Cache({
namespace: new Namespace("cache"),
adapter: memoryCacheAdapter,
eventBus: new EventBus({
// We assign distinct namespaces to MemoryCacheAdapter and RedisCacheAdapter to isolate their events.
namespace: new Namespace(["memory-cache", "event-bus"]),
adapter: redisPubSubEventBusAdapter,
}),
});

const redisCacheAdapter = new RedisCacheAdapter({
serde,
database: new Redis("YOUR_REDIS_CONNECTION_STRING"),
});
const redisCache = new Cache({
namespace: new Namespace("cache"),
adapter: redisCacheAdapter,
eventBus: new EventBus({
// We assign distinct namespaces to MemoryCacheAdapter and RedisCacheAdapter to isolate their events.
namespace: new Namespace(["redis-cache", "event-bus"]),
adapter: redisPubSubEventBusAdapter,
}),
});

Seperating manipulating cache and listening

The library includes two additional contracts:

This seperation makes it easy to visually distinguish the two contracts, making it immediately obvious that they serve different purposes.

import type {
ICache,
ICahceBase,
ICacheListenable,
CACHE_EVENTS,
} from "@daiso-tech/core/cache/contracts";

function manipulatingFunc(cache: ICahceBase): Promise<void> {
// You cannot access the listener methods
// You will get typescript error if you try

await cache.add("a", 1);
}
function listenerFunc(cacheListenable: ICacheListenable): Promise<void> {
// You cannot access the cache methods
// You will get typescript error if you try

await cacheListenable.addListener(CACHE_EVENTS.WRITTEN, (event) => {
console.log("EVENT:", event);
});
}

await listenerFunc(cache);
await manipulatingFunc(cache);