diff --git a/apps/website/docs/api-reference/cache/classes/memory-cache.mdx b/apps/website/docs/api-reference/cache/classes/memory-cache.mdx index 0ff69848..157b1210 100644 --- a/apps/website/docs/api-reference/cache/classes/memory-cache.mdx +++ b/apps/website/docs/api-reference/cache/classes/memory-cache.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## MemoryCache - + MemoryCache is an in-memory cache provider that implements the CacheProvider interface. It stores cache entries in a Map and supports basic operations like get, set, exists, delete, clear, and expire. diff --git a/apps/website/docs/api-reference/cache/classes/redis-cache.mdx b/apps/website/docs/api-reference/cache/classes/redis-cache.mdx new file mode 100644 index 00000000..cde5269a --- /dev/null +++ b/apps/website/docs/api-reference/cache/classes/redis-cache.mdx @@ -0,0 +1,124 @@ +--- +title: "RedisCache" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RedisCache + + + +A cache provider that uses Redis as the cache store. + + + +*Example* + +```ts +const redisCache = new RedisCache(); +new CommandKit({ +... +cacheProvider: redisCache, +}); +``` + +```ts title="Signature" +class RedisCache extends CacheProvider { + public redis: Redis; + public serialize: SerializeFunction; + public deserialize: DeserializeFunction; + constructor() + constructor(redis: Redis) + constructor(redis: RedisOptions) + constructor(redis?: Redis | RedisOptions) + get(key: string) => Promise | undefined>; + set(key: string, value: T, ttl?: number) => Promise; + clear() => Promise; + delete(key: string) => Promise; + exists(key: string) => Promise; + expire(key: string, ttl: number) => Promise; +} +``` +* Extends: CacheProvider + + + +
+ +### redis + + + + +### serialize + +SerializeFunction`} /> + +Serialize function used to serialize values before storing them in the cache. +By default, it uses `JSON.stringify`. +### deserialize + +DeserializeFunction`} /> + +Deserialize function used to deserialize values before returning them from the cache. +By default, it uses `JSON.parse`. +### constructor + + RedisCache`} /> + +Create a new RedisCache instance. +### constructor + + RedisCache`} /> + +Create a new RedisCache instance with the provided Redis client. +### constructor + + RedisCache`} /> + +Create a new RedisCache instance with the provided Redis options. +### constructor + + RedisCache`} /> + + +### get + + Promise<CacheEntry<T> | undefined>`} /> + +Retrieve a value from the cache. +### set + + Promise<void>`} /> + +Store a value in the cache. +### clear + + Promise<void>`} /> + +Clear all entries from the cache. +### delete + + Promise<void>`} /> + +Delete a value from the cache. +### exists + + Promise<boolean>`} /> + +Check if a value exists in the cache. +### expire + + Promise<void>`} /> + +Set the time-to-live for a cache entry. + + +
diff --git a/apps/website/docs/api-reference/cache/functions/cache-life.mdx b/apps/website/docs/api-reference/cache/functions/cache-life.mdx index 2b1bfbe0..579f6ce7 100644 --- a/apps/website/docs/api-reference/cache/functions/cache-life.mdx +++ b/apps/website/docs/api-reference/cache/functions/cache-life.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## cacheLife - + Sets the TTL for the current cache operation diff --git a/apps/website/docs/api-reference/cache/functions/cache-tag.mdx b/apps/website/docs/api-reference/cache/functions/cache-tag.mdx index 8d739f2f..2694de6f 100644 --- a/apps/website/docs/api-reference/cache/functions/cache-tag.mdx +++ b/apps/website/docs/api-reference/cache/functions/cache-tag.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## cacheTag - + Sets a custom identifier for the current cache operation diff --git a/apps/website/docs/api-reference/cache/functions/cleanup.mdx b/apps/website/docs/api-reference/cache/functions/cleanup.mdx index fa37868b..5bf058ce 100644 --- a/apps/website/docs/api-reference/cache/functions/cleanup.mdx +++ b/apps/website/docs/api-reference/cache/functions/cleanup.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## cleanup - + Cleans up stale cache entries. diff --git a/apps/website/docs/api-reference/cache/functions/is-cached-function.mdx b/apps/website/docs/api-reference/cache/functions/is-cached-function.mdx index 44ba721a..6a1273a2 100644 --- a/apps/website/docs/api-reference/cache/functions/is-cached-function.mdx +++ b/apps/website/docs/api-reference/cache/functions/is-cached-function.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## isCachedFunction - + Checks if a function is wrapped with cache functionality diff --git a/apps/website/docs/api-reference/cache/functions/revalidate-tag.mdx b/apps/website/docs/api-reference/cache/functions/revalidate-tag.mdx index eff56e68..8608e74a 100644 --- a/apps/website/docs/api-reference/cache/functions/revalidate-tag.mdx +++ b/apps/website/docs/api-reference/cache/functions/revalidate-tag.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## revalidateTag - + Marks cache entries for invalidation by their tag. The invalidation only happens when the path is next visited. diff --git a/apps/website/docs/api-reference/cache/interfaces/cache-context.mdx b/apps/website/docs/api-reference/cache/interfaces/cache-context.mdx index 8a40167d..96a5492f 100644 --- a/apps/website/docs/api-reference/cache/interfaces/cache-context.mdx +++ b/apps/website/docs/api-reference/cache/interfaces/cache-context.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CacheContext - + Context for managing cache operations within an async scope diff --git a/apps/website/docs/api-reference/cache/interfaces/cache-metadata.mdx b/apps/website/docs/api-reference/cache/interfaces/cache-metadata.mdx index f1b51dd7..bd2856e8 100644 --- a/apps/website/docs/api-reference/cache/interfaces/cache-metadata.mdx +++ b/apps/website/docs/api-reference/cache/interfaces/cache-metadata.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CacheMetadata - + Configuration options for cache behavior diff --git a/apps/website/docs/api-reference/cache/types/awaitable.mdx b/apps/website/docs/api-reference/cache/types/awaitable.mdx new file mode 100644 index 00000000..515bba18 --- /dev/null +++ b/apps/website/docs/api-reference/cache/types/awaitable.mdx @@ -0,0 +1,22 @@ +--- +title: "Awaitable" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## Awaitable + + + + + +```ts title="Signature" +type Awaitable = T | Promise +``` diff --git a/apps/website/docs/api-reference/cache/types/deserialize-function.mdx b/apps/website/docs/api-reference/cache/types/deserialize-function.mdx new file mode 100644 index 00000000..a9f63795 --- /dev/null +++ b/apps/website/docs/api-reference/cache/types/deserialize-function.mdx @@ -0,0 +1,22 @@ +--- +title: "DeserializeFunction" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## DeserializeFunction + + + + + +```ts title="Signature" +type DeserializeFunction = (value: string) => Awaitable +``` diff --git a/apps/website/docs/api-reference/cache/types/index.mdx b/apps/website/docs/api-reference/cache/types/index.mdx new file mode 100644 index 00000000..224a2db8 --- /dev/null +++ b/apps/website/docs/api-reference/cache/types/index.mdx @@ -0,0 +1,16 @@ +--- +title: "Type Aliases" +isDefaultIndex: true +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +import DocCardList from '@theme/DocCardList'; + + \ No newline at end of file diff --git a/apps/website/docs/api-reference/cache/types/serialize-function.mdx b/apps/website/docs/api-reference/cache/types/serialize-function.mdx new file mode 100644 index 00000000..46e0012c --- /dev/null +++ b/apps/website/docs/api-reference/cache/types/serialize-function.mdx @@ -0,0 +1,22 @@ +--- +title: "SerializeFunction" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## SerializeFunction + + + + + +```ts title="Signature" +type SerializeFunction = (value: any) => Awaitable +``` diff --git a/apps/website/docs/api-reference/redis/classes/redis-cache.mdx b/apps/website/docs/api-reference/redis/classes/redis-cache.mdx index 8bd58b3a..8f363f28 100644 --- a/apps/website/docs/api-reference/redis/classes/redis-cache.mdx +++ b/apps/website/docs/api-reference/redis/classes/redis-cache.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## RedisCache - + A cache provider that uses Redis as the cache store. @@ -23,10 +23,7 @@ A cache provider that uses Redis as the cache store. ```ts const redisCache = new RedisCache(); -new CommandKit({ -... -cacheProvider: redisCache, -}); +setCacheProvider(redisCache); ``` ```ts title="Signature" diff --git a/apps/website/docs/api-reference/redis/classes/redis-plugin.mdx b/apps/website/docs/api-reference/redis/classes/redis-plugin.mdx index cb39432e..c19a247e 100644 --- a/apps/website/docs/api-reference/redis/classes/redis-plugin.mdx +++ b/apps/website/docs/api-reference/redis/classes/redis-plugin.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## RedisPlugin - + diff --git a/apps/website/docs/api-reference/redis/functions/redis.mdx b/apps/website/docs/api-reference/redis/functions/redis.mdx index 95fe7d5a..6e28c847 100644 --- a/apps/website/docs/api-reference/redis/functions/redis.mdx +++ b/apps/website/docs/api-reference/redis/functions/redis.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## redis - + Create a new Redis plugin instance. diff --git a/apps/website/docs/api-reference/redis/types/awaitable.mdx b/apps/website/docs/api-reference/redis/types/awaitable.mdx index 2c51e358..3bcca9cd 100644 --- a/apps/website/docs/api-reference/redis/types/awaitable.mdx +++ b/apps/website/docs/api-reference/redis/types/awaitable.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## Awaitable - + diff --git a/apps/website/docs/api-reference/redis/types/deserialize-function.mdx b/apps/website/docs/api-reference/redis/types/deserialize-function.mdx index a9146253..b222ff5c 100644 --- a/apps/website/docs/api-reference/redis/types/deserialize-function.mdx +++ b/apps/website/docs/api-reference/redis/types/deserialize-function.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## DeserializeFunction - + diff --git a/apps/website/docs/api-reference/redis/types/serialize-function.mdx b/apps/website/docs/api-reference/redis/types/serialize-function.mdx index a3d819c2..2d0dc49f 100644 --- a/apps/website/docs/api-reference/redis/types/serialize-function.mdx +++ b/apps/website/docs/api-reference/redis/types/serialize-function.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## SerializeFunction - + diff --git a/apps/website/docs/guide/05-official-plugins/03-commandkit-cache.mdx b/apps/website/docs/guide/05-official-plugins/03-commandkit-cache.mdx index 49824b81..f943d0f9 100644 --- a/apps/website/docs/guide/05-official-plugins/03-commandkit-cache.mdx +++ b/apps/website/docs/guide/05-official-plugins/03-commandkit-cache.mdx @@ -207,7 +207,7 @@ Redis: ```ts title="commandkit.config.ts" import { defineConfig } from 'commandkit'; import { cache, setCacheProvider } from '@commandkit/cache'; -import { RedisCache } from '@commandkit/redis'; +import { RedisCache } from '@commandkit/cache/redis'; // Set up Redis as the cache provider setCacheProvider( @@ -240,12 +240,3 @@ setInterval( 24 * 60 * 60 * 1000, ); // Run daily ``` - -:::tip - -The `@commandkit/cache` plugin works seamlessly with other CommandKit -features and plugins. You can use it alongside the -[`@commandkit/redis`](./07-commandkit-redis.mdx) plugin for -distributed caching across multiple bot instances. - -::: diff --git a/apps/website/docs/guide/05-official-plugins/07-commandkit-redis.mdx b/apps/website/docs/guide/05-official-plugins/07-commandkit-redis.mdx deleted file mode 100644 index f7056626..00000000 --- a/apps/website/docs/guide/05-official-plugins/07-commandkit-redis.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: '@commandkit/redis' ---- - -The `@commandkit/redis` plugin provides a cache provider for -CommandKit that allows you to store data in Redis. It works out of the -box with the [`@commandkit/cache`](./03-commandkit-cache.mdx) plugin. - -## Installation - -```sh npm2yarn -npm install @commandkit/redis@next -``` - -## Usage - -This plugin will automatically register the Redis cache provider with -your CommandKit instance. - -```js -import { defineConfig } from 'commandkit'; -import { redis } from '@commandkit/redis'; - -export default defineConfig({ - plugins: [redis()], -}); -``` - -Once configured, your cache functions will automatically use Redis as -the storage backend: - -```ts -async function getCachedData() { - 'use cache'; // This directive enables caching for the function - - // Your data retrieval logic - const data = await getFromDatabase('something'); - - return data; -} -``` - -## Manual Configuration - -If you need more control over the Redis client configuration, you can -set up the cache provider manually instead of using the plugin: - -```ts -import { setCacheProvider } from '@commandkit/cache'; -import { RedisCacheProvider } from '@commandkit/redis'; -import { Redis } from 'ioredis'; - -// Configure the Redis client with custom options -const redis = new Redis({ - host: 'your-redis-host', - port: 6379, - // Add other Redis options as needed -}); - -const redisProvider = new RedisCacheProvider(redis); - -// Register the provider with CommandKit -setCacheProvider(redisProvider); -``` - -This approach gives you full control over the Redis client -configuration while still integrating with CommandKit's caching -system. diff --git a/packages/cache/README.md b/packages/cache/README.md index 2aca340c..da4129be 100644 --- a/packages/cache/README.md +++ b/packages/cache/README.md @@ -24,7 +24,7 @@ Next, you can define advanced configurations for the plugin if needed. For examp ```ts import { setCacheProvider } from '@commandkit/cache'; -import { RedisCache } from '@commandkit/redis'; +import { RedisCache } from '@commandkit/cache/providers/redis'; setCacheProvider(new RedisCache({...})); ``` diff --git a/packages/cache/package.json b/packages/cache/package.json index 94577955..1c625173 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -7,6 +7,20 @@ "files": [ "dist" ], + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./redis": { + "import": "./dist/providers/redis.js", + "types": "./dist/providers/redis.d.ts" + }, + "./memory-cache": { + "import": "./dist/providers/memory-cache.js", + "types": "./dist/providers/memory-cache.d.ts" + } + }, "scripts": { "check-types": "tsc --noEmit", "build": "tsc" @@ -34,6 +48,7 @@ "devDependencies": { "@types/ms": "^2.0.0", "commandkit": "workspace:*", + "ioredis": "^5.8.1", "tsconfig": "workspace:*", "typescript": "catalog:build" }, diff --git a/packages/cache/src/cache-plugin.ts b/packages/cache/src/cache-plugin.ts index e34c31e4..e7957b4a 100644 --- a/packages/cache/src/cache-plugin.ts +++ b/packages/cache/src/cache-plugin.ts @@ -1,6 +1,6 @@ import { Logger, RuntimePlugin } from 'commandkit'; import type { CacheProvider } from './cache-provider'; -import { MemoryCache } from './memory-cache'; +import { MemoryCache } from './providers/memory-cache'; let cacheProvider: CacheProvider | null = null; diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts index b5a31f1f..4c9d95ef 100644 --- a/packages/cache/src/index.ts +++ b/packages/cache/src/index.ts @@ -6,7 +6,7 @@ import { UseCacheDirectivePlugin } from './use-cache-directive'; import { CachePlugin } from './cache-plugin'; export * from './cache-provider'; -export * from './memory-cache'; +export * from './providers/memory-cache'; export * from './use-cache'; export * from './use-cache-directive'; export * from './cache-plugin'; diff --git a/packages/cache/src/memory-cache.ts b/packages/cache/src/providers/memory-cache.ts similarity index 97% rename from packages/cache/src/memory-cache.ts rename to packages/cache/src/providers/memory-cache.ts index 668f00df..9c038383 100644 --- a/packages/cache/src/memory-cache.ts +++ b/packages/cache/src/providers/memory-cache.ts @@ -1,4 +1,4 @@ -import { CacheEntry, CacheProvider } from './cache-provider'; +import { CacheEntry, CacheProvider } from '../cache-provider'; /** * MemoryCache is an in-memory cache provider that implements the CacheProvider interface. diff --git a/packages/cache/src/providers/redis.ts b/packages/cache/src/providers/redis.ts new file mode 100644 index 00000000..f7233841 --- /dev/null +++ b/packages/cache/src/providers/redis.ts @@ -0,0 +1,134 @@ +import { Redis, type RedisOptions } from 'ioredis'; +import { CacheProvider, CacheEntry } from '../cache-provider'; + +export type Awaitable = T | Promise; +export type SerializeFunction = (value: any) => Awaitable; +export type DeserializeFunction = (value: string) => Awaitable; + +/** + * A cache provider that uses Redis as the cache store. + * @example const redisCache = new RedisCache(); + * new CommandKit({ + * ... + * cacheProvider: redisCache, + * }); + */ +export class RedisCache extends CacheProvider { + public redis: Redis; + + /** + * Serialize function used to serialize values before storing them in the cache. + * By default, it uses `JSON.stringify`. + */ + public serialize: SerializeFunction; + + /** + * Deserialize function used to deserialize values before returning them from the cache. + * By default, it uses `JSON.parse`. + */ + public deserialize: DeserializeFunction; + + /** + * Create a new RedisCache instance. + */ + public constructor(); + /** + * Create a new RedisCache instance with the provided Redis client. + * @param redis The Redis client to use. + */ + public constructor(redis: Redis); + /** + * Create a new RedisCache instance with the provided Redis options. + * @param redis The Redis client to use. + */ + public constructor(redis: RedisOptions); + public constructor(redis?: Redis | RedisOptions) { + super(); + + if (redis instanceof Redis) { + this.redis = redis; + } else { + this.redis = new Redis(redis ?? {}); + } + + this.serialize = JSON.stringify; + this.deserialize = JSON.parse; + } + + /** + * Retrieve a value from the cache. + * @param key The key to retrieve the value for. + * @returns The value stored in the cache, or `undefined` if it does not exist. + */ + public async get(key: string): Promise | undefined> { + const value = await this.redis.get(key); + + if (value === null) { + return undefined; + } + + const entry = this.deserialize(value) as CacheEntry; + if (entry.ttl && Date.now() > entry.ttl) { + await this.delete(key); + return undefined; + } + + return entry; + } + + /** + * Store a value in the cache. + * @param key The key to store the value under. + * @param value The value to store in the cache. + * @param ttl The time-to-live for the cache entry in milliseconds. + */ + public async set(key: string, value: T, ttl?: number): Promise { + const entry: CacheEntry = { + value, + ttl: ttl != null ? Date.now() + ttl : undefined, + }; + + const serialized = this.serialize(entry); + const finalValue = + serialized instanceof Promise ? await serialized : serialized; + + if (typeof ttl === 'number') { + await this.redis.set(key, finalValue, 'PX', ttl); + } else { + await this.redis.set(key, finalValue); + } + } + + /** + * Clear all entries from the cache. + */ + public async clear(): Promise { + await this.redis.flushall(); + } + + /** + * Delete a value from the cache. + * @param key The key to delete the value for. + */ + public async delete(key: string): Promise { + await this.redis.del(key); + } + + /** + * Check if a value exists in the cache. + * @param key The key to check for. + * @returns True if the key exists in the cache, false otherwise. + */ + public async exists(key: string): Promise { + return Boolean(await this.redis.exists(key)); + } + + /** + * Set the time-to-live for a cache entry. + * @param key The key to set the time-to-live for. + * @param ttl The time-to-live value in milliseconds. + */ + public async expire(key: string, ttl: number): Promise { + await this.redis.pexpire(key, ttl); + } +} diff --git a/packages/cache/src/use-cache.ts b/packages/cache/src/use-cache.ts index a54ac194..4d07da5d 100644 --- a/packages/cache/src/use-cache.ts +++ b/packages/cache/src/use-cache.ts @@ -6,7 +6,6 @@ import { getCommandKit, } from 'commandkit'; import { AnalyticsEvents } from 'commandkit/analytics'; -import { randomUUID } from 'node:crypto'; import ms, { type StringValue } from 'ms'; import { getCacheProvider } from './cache-plugin'; import { createHash } from './utils'; @@ -18,18 +17,12 @@ const fnStore = new Map< key: string; hash: string; ttl?: number; - original: GenericFunction; - memo: GenericFunction; tags: Set; lastAccess: number; } >(); -const CACHE_FN_ID = `__cache_fn_id_${Date.now()}__${Math.random()}__`; const CACHED_FN_SYMBOL = Symbol('commandkit.cache.sentinel'); -// WeakMap to store function metadata without preventing garbage collection -const fnMetadata = new WeakMap(); - /** * Context for managing cache operations within an async scope */ @@ -68,40 +61,19 @@ export interface CacheMetadata { * } * ``` */ -function useCache>( - fn: F, - id?: string, - params?: CacheMetadata, -): F { - const isLocal = id === CACHE_FN_ID; - - if (id && !isLocal) { - throw new Error('Illegal use of cache function.'); - } - - // Get or create function metadata - let metadata = fnMetadata.get(fn); - if (!metadata) { - metadata = randomUUID(); - fnMetadata.set(fn, metadata); - } +function useCache>(fn: F): F { + // assign unique id to the function to avoid collisions with other functions + const metadata = crypto.randomUUID(); const memo = (async (...args) => { const analytics = getCommandKit()?.analytics; const keyHash = createHash(metadata, args); - const resolvedTTL = - isLocal && params?.ttl != null - ? typeof params.ttl === 'string' - ? ms(params.ttl as StringValue) - : params.ttl - : null; - return cacheContext.run( { params: { - ttl: resolvedTTL, - tags: new Set(params?.tags ?? []), + ttl: null, + tags: new Set(), }, }, async () => { @@ -151,8 +123,6 @@ function useCache>( key: keyHash, hash: keyHash, ttl: ttl ?? undefined, - original: fn, - memo, tags: context.params.tags, lastAccess: Date.now(), }); diff --git a/packages/commandkit/src/index.ts b/packages/commandkit/src/index.ts index a097dd97..12932455 100644 --- a/packages/commandkit/src/index.ts +++ b/packages/commandkit/src/index.ts @@ -30,6 +30,7 @@ export { debounce, defer, } from './utils/utilities'; +export { warnDeprecated, emitWarning, warnUnstable } from './utils/warning'; export { toFileURL } from './utils/resolve-file-url'; export * from './app/interrupt/signals'; export type { CommandKitHMREvent } from './utils/dev-hooks'; diff --git a/packages/redis/README.md b/packages/redis/README.md index b34686df..49a3d686 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -10,6 +10,9 @@ npm install @commandkit/redis ## Usage +> [!WARNING] +> `RedisCache` from `@commandkit/redis` is deprecated. Import `RedisCache` from `@commandkit/cache/redis` instead. + This package provides a commandkit plugin that automatically registers the cache provider with the commandkit instance. ```js diff --git a/packages/redis/src/cache-storage.ts b/packages/redis/src/cache-storage.ts new file mode 100644 index 00000000..dc9c9d03 --- /dev/null +++ b/packages/redis/src/cache-storage.ts @@ -0,0 +1,139 @@ +import { CacheProvider, CacheEntry } from '@commandkit/cache'; +import { warnDeprecated } from 'commandkit'; +import Redis, { type RedisOptions } from 'ioredis'; + +export type Awaitable = T | Promise; +export type SerializeFunction = (value: any) => Awaitable; +export type DeserializeFunction = (value: string) => Awaitable; + +/** + * A cache provider that uses Redis as the cache store. + * @example const redisCache = new RedisCache(); + * setCacheProvider(redisCache); + * @deprecated Import `RedisCache` from `@commandkit/cache/redis` instead. + */ +export class RedisCache extends CacheProvider { + public redis: Redis; + + /** + * Serialize function used to serialize values before storing them in the cache. + * By default, it uses `JSON.stringify`. + */ + public serialize: SerializeFunction; + + /** + * Deserialize function used to deserialize values before returning them from the cache. + * By default, it uses `JSON.parse`. + */ + public deserialize: DeserializeFunction; + + /** + * Create a new RedisCache instance. + */ + public constructor(); + /** + * Create a new RedisCache instance with the provided Redis client. + * @param redis The Redis client to use. + */ + public constructor(redis: Redis); + /** + * Create a new RedisCache instance with the provided Redis options. + * @param redis The Redis client to use. + */ + public constructor(redis: RedisOptions); + public constructor(redis?: Redis | RedisOptions) { + warnDeprecated({ + what: 'RedisCache', + where: '@commandkit/redis', + message: 'Import `RedisCache` from `@commandkit/cache/redis` instead.', + }); + + super(); + + if (redis instanceof Redis) { + this.redis = redis; + } else { + this.redis = new Redis(redis ?? {}); + } + + this.serialize = JSON.stringify; + this.deserialize = JSON.parse; + } + + /** + * Retrieve a value from the cache. + * @param key The key to retrieve the value for. + * @returns The value stored in the cache, or `undefined` if it does not exist. + */ + public async get(key: string): Promise | undefined> { + const value = await this.redis.get(key); + + if (value === null) { + return undefined; + } + + const entry = this.deserialize(value) as CacheEntry; + if (entry.ttl && Date.now() > entry.ttl) { + await this.delete(key); + return undefined; + } + + return entry; + } + + /** + * Store a value in the cache. + * @param key The key to store the value under. + * @param value The value to store in the cache. + * @param ttl The time-to-live for the cache entry in milliseconds. + */ + public async set(key: string, value: T, ttl?: number): Promise { + const entry: CacheEntry = { + value, + ttl: ttl != null ? Date.now() + ttl : undefined, + }; + + const serialized = this.serialize(entry); + const finalValue = + serialized instanceof Promise ? await serialized : serialized; + + if (typeof ttl === 'number') { + await this.redis.set(key, finalValue, 'PX', ttl); + } else { + await this.redis.set(key, finalValue); + } + } + + /** + * Clear all entries from the cache. + */ + public async clear(): Promise { + await this.redis.flushall(); + } + + /** + * Delete a value from the cache. + * @param key The key to delete the value for. + */ + public async delete(key: string): Promise { + await this.redis.del(key); + } + + /** + * Check if a value exists in the cache. + * @param key The key to check for. + * @returns True if the key exists in the cache, false otherwise. + */ + public async exists(key: string): Promise { + return Boolean(await this.redis.exists(key)); + } + + /** + * Set the time-to-live for a cache entry. + * @param key The key to set the time-to-live for. + * @param ttl The time-to-live value in milliseconds. + */ + public async expire(key: string, ttl: number): Promise { + await this.redis.pexpire(key, ttl); + } +} diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index c65d6a52..f76fd1b6 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -1,138 +1,7 @@ -import { Redis, type RedisOptions } from 'ioredis'; +import { type RedisOptions } from 'ioredis'; import { CommandKitPluginRuntime, RuntimePlugin } from 'commandkit'; -import { CacheProvider, CacheEntry, setCacheProvider } from '@commandkit/cache'; - -export type Awaitable = T | Promise; -export type SerializeFunction = (value: any) => Awaitable; -export type DeserializeFunction = (value: string) => Awaitable; - -/** - * A cache provider that uses Redis as the cache store. - * @example const redisCache = new RedisCache(); - * new CommandKit({ - * ... - * cacheProvider: redisCache, - * }); - */ -export class RedisCache extends CacheProvider { - public redis: Redis; - - /** - * Serialize function used to serialize values before storing them in the cache. - * By default, it uses `JSON.stringify`. - */ - public serialize: SerializeFunction; - - /** - * Deserialize function used to deserialize values before returning them from the cache. - * By default, it uses `JSON.parse`. - */ - public deserialize: DeserializeFunction; - - /** - * Create a new RedisCache instance. - */ - public constructor(); - /** - * Create a new RedisCache instance with the provided Redis client. - * @param redis The Redis client to use. - */ - public constructor(redis: Redis); - /** - * Create a new RedisCache instance with the provided Redis options. - * @param redis The Redis client to use. - */ - public constructor(redis: RedisOptions); - public constructor(redis?: Redis | RedisOptions) { - super(); - - if (redis instanceof Redis) { - this.redis = redis; - } else { - this.redis = new Redis(redis ?? {}); - } - - this.serialize = JSON.stringify; - this.deserialize = JSON.parse; - } - - /** - * Retrieve a value from the cache. - * @param key The key to retrieve the value for. - * @returns The value stored in the cache, or `undefined` if it does not exist. - */ - public async get(key: string): Promise | undefined> { - const value = await this.redis.get(key); - - if (value === null) { - return undefined; - } - - const entry = this.deserialize(value) as CacheEntry; - if (entry.ttl && Date.now() > entry.ttl) { - await this.delete(key); - return undefined; - } - - return entry; - } - - /** - * Store a value in the cache. - * @param key The key to store the value under. - * @param value The value to store in the cache. - * @param ttl The time-to-live for the cache entry in milliseconds. - */ - public async set(key: string, value: T, ttl?: number): Promise { - const entry: CacheEntry = { - value, - ttl: ttl != null ? Date.now() + ttl : undefined, - }; - - const serialized = this.serialize(entry); - const finalValue = - serialized instanceof Promise ? await serialized : serialized; - - if (typeof ttl === 'number') { - await this.redis.set(key, finalValue, 'PX', ttl); - } else { - await this.redis.set(key, finalValue); - } - } - - /** - * Clear all entries from the cache. - */ - public async clear(): Promise { - await this.redis.flushall(); - } - - /** - * Delete a value from the cache. - * @param key The key to delete the value for. - */ - public async delete(key: string): Promise { - await this.redis.del(key); - } - - /** - * Check if a value exists in the cache. - * @param key The key to check for. - * @returns True if the key exists in the cache, false otherwise. - */ - public async exists(key: string): Promise { - return Boolean(await this.redis.exists(key)); - } - - /** - * Set the time-to-live for a cache entry. - * @param key The key to set the time-to-live for. - * @param ttl The time-to-live value in milliseconds. - */ - public async expire(key: string, ttl: number): Promise { - await this.redis.pexpire(key, ttl); - } -} +import { setCacheProvider } from '@commandkit/cache'; +import { RedisCache } from './cache-storage'; export class RedisPlugin extends RuntimePlugin { public readonly name = 'RedisPlugin'; @@ -154,4 +23,4 @@ export function redis(options?: RedisOptions) { export * from './ratelimit-storage'; export * from './mutex-storage'; export * from './semaphore-storage'; -export { RedisCache as RedisCacheProvider }; +export { RedisCache as RedisCacheProvider, RedisCache }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index daa085f4..c4f78500 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,6 +251,9 @@ importers: commandkit: specifier: workspace:* version: link:../commandkit + ioredis: + specifier: ^5.8.1 + version: 5.8.1 tsconfig: specifier: workspace:* version: link:../tsconfig @@ -2348,9 +2351,6 @@ packages: '@types/node': optional: true - '@ioredis/commands@1.2.0': - resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - '@ioredis/commands@1.4.0': resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} @@ -6254,10 +6254,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.6.1: - resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==} - engines: {node: '>=12.22.0'} - ioredis@5.8.1: resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==} engines: {node: '>=12.22.0'} @@ -9776,7 +9772,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.2 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -11134,7 +11130,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11462,7 +11458,7 @@ snapshots: dependencies: '@msgpack/msgpack': 3.1.2 '@vladfrangu/async_event_emitter': 2.4.6 - ioredis: 5.6.1 + ioredis: 5.8.1 transitivePeerDependencies: - supports-color @@ -12659,8 +12655,6 @@ snapshots: optionalDependencies: '@types/node': 22.18.8 - '@ioredis/commands@1.2.0': {} - '@ioredis/commands@1.4.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -14886,7 +14880,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 + debug: 4.4.3 http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -16398,7 +16392,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -17018,20 +17012,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@5.6.1: - dependencies: - '@ioredis/commands': 1.2.0 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.8.1: dependencies: '@ioredis/commands': 1.4.0 @@ -19587,7 +19567,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -19689,7 +19669,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -20542,7 +20522,7 @@ snapshots: vite-node@3.2.4(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.0.6(@types/node@22.18.8)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.6)(yaml@2.8.1)