Skip to main content

Make Your Own Cache Driver

Warlock ships with several cache drivers out of the box:

But you can easily make your own! All you need to do is implement the CacheDriver interface, or extend BaseCacheDriver for common logic.

Example: Memcached Driver

Let's say we want to implement a Memcached cache driver.

import { BaseCacheDriver, CacheDriver } from "@warlock.js/core";
import type { GenericObject } from "@mongez/reinforcements";
import Memcached from "memcached";

export interface MemCachedCacheDriverOptions {
host: string;
port: number;
globalPrefix?: string;
ttl?: number;
}

export class MemCachedCacheDriver
extends BaseCacheDriver<Memcached, MemCachedCacheDriverOptions>
implements CacheDriver<Memcached, MemCachedCacheDriverOptions>
{
public name = "memcached";

public async connect() {
this.log("connecting");

this.client = new Memcached(`${this.options.host}:${this.options.port}`);

this.log("connected");
return this;
}

public async disconnect() {
this.log("disconnecting");

this.client.end();

this.log("disconnected");
}

public async removeNamespace(namespace: string) {
namespace = await this.parseKey(namespace);

this.log("clearing", namespace);

// Note: Memcached doesn't support pattern-based deletion
// This is a limitation of the driver
this.log("cleared", namespace);
}

public async set(key: string | GenericObject, value: any, ttl?: number) {
const parsedKey = await this.parseKey(key);

this.log("caching", parsedKey);

const data = this.prepareDataForStorage(value, ttl);

return new Promise((resolve, reject) => {
this.client.set(parsedKey, JSON.stringify(data), ttl || 0, (error) => {
if (error) {
this.log("error", error.message);
return reject(error);
}

this.log("cached", parsedKey);
resolve(value);
});
});
}

public async get(key: string | GenericObject) {
const parsedKey = await this.parseKey(key);

this.log("fetching", parsedKey);

return new Promise((resolve, reject) => {
this.client.get(parsedKey, (error, value) => {
if (error) {
this.log("error", error.message);
return reject(error);
}

if (!value) {
this.log("notFound", parsedKey);
return resolve(null);
}

try {
const data = JSON.parse(value);
resolve(this.parseCachedData(parsedKey, data));
} catch (parseError) {
this.log("error", "Failed to parse cached data");
resolve(null);
}
});
});
}

public async remove(key: string | GenericObject) {
const parsedKey = await this.parseKey(key);

this.log("removing", parsedKey);

return new Promise((resolve, reject) => {
this.client.del(parsedKey, (error) => {
if (error) {
this.log("error", error.message);
return reject(error);
}

this.log("removed", parsedKey);
resolve();
});
});
}

public async flush() {
this.log("flushing");

return new Promise((resolve, reject) => {
this.client.flush((error) => {
if (error) {
this.log("error", error.message);
return reject(error);
}

this.log("flushed");
resolve();
});
});
}
}

Example: Simple In-Memory Driver

Here's a simple in-memory driver that extends BaseCacheDriver:

import { BaseCacheDriver, CacheDriver } from "@warlock.js/core";
import type { GenericObject } from "@mongez/reinforcements";

interface SimpleMemoryOptions {
globalPrefix?: string;
ttl?: number;
}

export class SimpleMemoryCacheDriver
extends BaseCacheDriver<SimpleMemoryCacheDriver, SimpleMemoryOptions>
implements CacheDriver<SimpleMemoryCacheDriver, SimpleMemoryOptions>
{
public name = "simpleMemory";

private storage = new Map<string, any>();

public async removeNamespace(namespace: string) {
namespace = await this.parseKey(namespace);

this.log("clearing", namespace);

for (const key of this.storage.keys()) {
if (key.startsWith(namespace)) {
this.storage.delete(key);
}
}

this.log("cleared", namespace);
}

public async set(key: string | GenericObject, value: any, ttl?: number) {
const parsedKey = await this.parseKey(key);

this.log("caching", parsedKey);

const data = this.prepareDataForStorage(value, ttl);
this.storage.set(parsedKey, data);

this.log("cached", parsedKey);
return value;
}

public async get(key: string | GenericObject) {
const parsedKey = await this.parseKey(key);

this.log("fetching", parsedKey);

const data = this.storage.get(parsedKey);
if (!data) {
this.log("notFound", parsedKey);
return null;
}

return this.parseCachedData(parsedKey, data);
}

public async remove(key: string | GenericObject) {
const parsedKey = await this.parseKey(key);

this.log("removing", parsedKey);

this.storage.delete(parsedKey);

this.log("removed", parsedKey);
}

public async flush() {
this.log("flushing");

this.storage.clear();

this.log("flushed");
}
}

Registering Your Driver

After creating your driver, register it in your cache configuration:

src/config/cache.ts
import { CacheConfigurations } from "@warlock.js/core";
import { MemCachedCacheDriver } from "./drivers/MemCachedCacheDriver";

const cacheConfigurations: CacheConfigurations = {
drivers: {
memcached: MemCachedCacheDriver,
},
default: "memcached",
options: {
memcached: {
host: "localhost",
port: 11211,
globalPrefix: "myapp",
ttl: 3600,
},
},
};

export default cacheConfigurations;

Tips

  • Extend BaseCacheDriver to get logging, key parsing, and option handling for free.
  • Use the built-in prepareDataForStorage() and parseCachedData() methods for consistent TTL handling.
  • Implement proper error handling and logging for better debugging.
  • Consider the limitations of your cache backend (e.g., Memcached doesn't support pattern-based deletion).
  • Test your driver thoroughly with different data types and edge cases.

See Cache Driver Interface for all required methods and Base Cache Driver for available helper methods.