Skip to content
Warlock.js v4

Cache Manager

The cache manager is the main interface for working with cache in your application. It manages drivers, handles configuration, and provides a unified API for all cache operations.

import { cache } from "@warlock.js/cache";

Before using the cache, configure and initialize it:

src/main.ts
import { cache } from "@warlock.js/cache";
import cacheConfigurations from "./config/cache";
// Set configurations
cache.setCacheConfigurations(cacheConfigurations);
// Initialize (connects to the default driver)
await cache.init();

:::info Important @warlock.js/cache requires manual setup. Unlike the Warlock framework where initialization is automatic, you must call setCacheConfigurations() and init() before using any cache operations. :::

The cache manager provides core cache operations that work with all drivers.

Store a value in cache. The third argument accepts three shapes:

import { cache, CACHE_FOR } from "@warlock.js/cache";
// 1. Number of seconds
await cache.set(`products.${productId}`, product, CACHE_FOR.ONE_HOUR);
await cache.set(`products.${productId}`, product, 3600);
// 2. Human-readable duration string (parsed via ms)
await cache.set(`products.${productId}`, product, "1h");
await cache.set(`session.${sessionId}`, session, "30m");
await cache.set(`report.${reportId}`, report, "7d");
// 3. Rich options object
await cache.set(`products.${productId}`, product, {
ttl: "1h",
tags: ["products"],
onConflict: "create",
});

If the third argument is omitted, the driver’s default TTL is used, or the value is stored forever (Infinity).

See Set Options for the full options-object reference — ttl, expiresAt, tags, onConflict, and per-call driver overrides.

Retrieve a value:

const product = await cache.get(`products.${productId}`); // null if not found

Delete a specific key:

await cache.remove(`products.${productId}`);

Clear the entire cache:

await cache.flush();

Check if a key exists without fetching:

const exists = await cache.has(`products.${productId}`);

Get and remove in one operation:

const verifyToken = await cache.pull(`users.${userId}.verify-token`);

Store without expiration:

await cache.forever("app.config", { version: "1.0.0" });
// Remove manually when no longer needed
await cache.remove("app.config");

remember() gets a value from cache, or if it doesn’t exist, executes a callback to generate it, caches the result, and returns it. It also prevents cache stampedes within a single process — concurrent callers for the same key share the in-flight promise.

const products = await cache.remember(
`products.category.${categoryId}`,
"30m",
async () => db.products.findByCategory(categoryId),
);
MethodWhen to useBehavior
set()You already have the valueStores it directly
remember()Value needs to be computed on cache missGets from cache OR runs the callback and caches the result
// set() — value in hand
const profile = await db.users.getProfile(userId);
await cache.set(`users.${userId}.profile`, profile, "1d");
// remember() — let the cache handle it
const profile = await cache.remember(`users.${userId}.profile`, "1d", () =>
db.users.getProfile(userId),
);

Atomically read, transform, and write a cached value:

await cache.update<User>(`user.${userId}`, (current) => ({
...(current ?? defaultUser),
lastSeenAt: Date.now(),
}));

See Update & Merge for the callback contract, TTL preservation rules, and driver support matrix.

Shallow-merge a partial object into an existing cached value:

await cache.merge<User>(`user.${userId}`, { theme: "dark" });

Obtain a typed list accessor for queue / stack / recent-N workflows:

const recent = cache.list<Event>("recent.events");
await recent.push(event);
const last10 = await recent.slice(0, 10);

See Cache Lists for the full accessor API.

Look up entries by cosine similarity instead of exact key match. Pair with set({ vector }):

await cache.set("doc.refunds", policy, { vector: await embed(policy.text) });
const hits = await cache.similar(await embed(userQuestion), {
topK: 5,
threshold: 0.7,
});

See Similarity Retrieval for the full API and capability matrix — only memory drivers and pg (with vector config) support it; others throw CacheUnsupportedError.

const currentDriver = cache.currentDriver;

currentDriver is undefined until init() or use() completes. Every data op guards with an init check, so calling get / set before init throws CacheDriverNotInitializedError.

Change the active driver:

await cache.use("redis");

use() is async because it loads the driver if not already loaded.

Most options live in setCacheConfigurations({ options }) — table names, TTLs, URL strings, anything declarative and serializable. Some options can only be built at runtime — pg’s client: pg.Pool, a pre-wired Redis client from your app’s connection module, anything you don’t want sitting inside a static config object.

Pass those via the second argument to use / load / driver:

import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
cache.setCacheConfigurations({
default: "pg",
drivers: { pg: PgCacheDriver },
options: { pg: { table: "cache" } }, // static defaults
});
await cache.use("pg", { client: pool }); // runtime injection

Runtime options merge over the config defaults per-key — runtime wins on collision:

// config: { ttl: 60, table: "cache" }
// runtime: { ttl: 120, client: pool }
// final: { ttl: 120, table: "cache", client: pool }

Once a driver is loaded, options are frozen on the instance. Calling load / driver / use a second time with new options throws CacheConfigurationError — no silent overwrites:

await cache.load("pg", { client: poolA });
await cache.load("pg", { client: poolB }); // throws

If you need a different configuration, register a second driver name (pg-tenant-b) — driver identity stays one-to-one with config.

Calling without options on an already-loaded driver still returns the cached instance silently. The throw only fires when options would otherwise be silently dropped.

When most writes go to your default driver but one call needs a different one, use the driver option instead of switching:

// Routes this single write to "redis" without mutating currentDriver
await cache.set("audit.event", event, { driver: "redis", ttl: "30d" });

Obtain a driver instance without changing the default:

const redisDriver = await cache.driver("redis");
const memoryDriver = await cache.driver("memory");
await redisDriver.set("key", "value");
await memoryDriver.set("temp", 123);

Add drivers at runtime:

import { CustomCacheDriver } from "./CustomCacheDriver";
cache.registerDriver("custom", CustomCacheDriver);
await cache.use("custom");
const options = cache.options;
cache.setOptions({
globalPrefix: "newprefix",
ttl: "2h",
});

All built-in drivers support a globalPrefix option to avoid key conflicts, especially on shared storage like Redis. Think of it like a database name — it separates your keys from other applications. The prefix can be a string or a function (the function form runs per call, which is great for per-tenant scoping).

Set it in your configuration or update at runtime with setOptions().

Namespaces help organize cache keys hierarchically using dot notation. See Namespaces for a complete guide, including cache.namespace(prefix, options?) for scoped handles.

await cache.set(`orders.${userId}.recent`, orders);
await cache.set(`orders.${userId}.total`, total);
// Remove all keys in a namespace
await cache.removeNamespace(`orders.${userId}`);
// Or grab a scoped handle when you'll touch the prefix more than once
const orders = cache.namespace(`orders.${userId}`, { ttl: "1h" });
await orders.set("recent", recent);
await orders.set("total", total);
await orders.clear();

Each has dedicated documentation:

Always use dot notation strings:

  • "user.1"

  • "posts.123.comments"

  • "app.config.settings"

  • { id: 1 } (objects work but reduce readability)

  • "user:1" (colons are rewritten to dots — be explicit)

:::tip Recommended Dot notation makes keys predictable, readable, and easier to debug. If you need to generate keys from complex data, see parseCacheKey. :::

Disconnect from the current driver:

await cache.disconnect();
  • Always call init() before using cache operations — operations throw CacheDriverNotInitializedError otherwise.
  • Use await cache.driver('name') to access a specific driver without changing the default — useful when you need to read from one driver and write to another in the same flow (e.g. rate-limit in memory, results in Redis).
  • Use { driver: "name" } in set options when the override is per-call — cleaner than switching and switching back.