@teamvelix/cache-redis 5.3.5 → 5.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -50,9 +50,15 @@ var RedisCacheAdapter = class {
50
50
  await this.redis.set(k, serialized);
51
51
  }
52
52
  if (options.tags?.length) {
53
- await Promise.all(
54
- options.tags.map((tag) => this.redis.sadd(this.tagKey(tag), k))
55
- );
53
+ const pipeline = this.redis.pipeline();
54
+ for (const tag of options.tags) {
55
+ const tKey = this.tagKey(tag);
56
+ pipeline.sadd(tKey, k);
57
+ if (ttlMs) {
58
+ pipeline.pexpire(tKey, ttlMs * 2);
59
+ }
60
+ }
61
+ await pipeline.exec();
56
62
  }
57
63
  }
58
64
  async delete(key) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/redis.adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import Redis from 'ioredis';\nimport type { RedisCacheAdapterOptions, StoredEntry } from './types.js';\n\n// ============================================================================\n// ICacheAdapter — inline pour éviter la dépendance circulaire et permettre\n// à @velix/cache-redis d'être utilisable sans velix-core installé\n// ============================================================================\n\nexport interface ICacheAdapter {\n get<T>(key: string): Promise<T | null>;\n set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;\n delete(key: string): Promise<void>;\n deleteByTag(tag: string): Promise<void>;\n deleteByPrefix(prefix: string): Promise<void>;\n clear(): Promise<void>;\n has(key: string): Promise<boolean>;\n}\n\nexport type CacheSetOptions = {\n /** TTL en millisecondes */\n ttl?: number;\n /** Tags pour l'invalidation groupée */\n tags?: string[];\n};\n\n// ============================================================================\n// Script Lua inline pour deleteByTag — atomicité garantie\n// ============================================================================\n\nconst DELETE_BY_TAG_LUA = `\nlocal keys = redis.call('SMEMBERS', KEYS[1])\nif #keys > 0 then\n redis.call('DEL', unpack(keys))\nend\nredis.call('DEL', KEYS[1])\nreturn #keys\n`;\n\n// ============================================================================\n// RedisCacheAdapter\n// ============================================================================\n\n/**\n * Adaptateur de cache Redis pour Velix.\n * Implémente ICacheAdapter via ioredis.\n *\n * @example\n * ```ts\n * import { RedisCacheAdapter } from '@velix/cache-redis';\n * import { defineConfig } from 'velix';\n *\n * export default defineConfig({\n * cache: {\n * adapter: new RedisCacheAdapter({ url: process.env.REDIS_URL }),\n * }\n * });\n * ```\n */\nexport class RedisCacheAdapter implements ICacheAdapter {\n private redis: Redis;\n private prefix: string;\n private defaultTTL?: number;\n\n constructor(options: RedisCacheAdapterOptions = {}) {\n this.redis = options.client ?? new Redis(options.url ?? 'redis://localhost:6379');\n this.prefix = options.keyPrefix ?? 'velix:';\n this.defaultTTL = options.defaultTTL;\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n private tagKey(tag: string): string {\n return `${this.prefix}tag:${tag}`;\n }\n\n // ── ICacheAdapter ─────────────────────────────────────────────────────────\n\n async get<T>(key: string): Promise<T | null> {\n const raw = await this.redis.get(this.key(key));\n if (!raw) return null;\n try {\n const entry = JSON.parse(raw) as StoredEntry<T>;\n return entry.value;\n } catch {\n return null;\n }\n }\n\n async set<T>(key: string, value: T, options: CacheSetOptions = {}): Promise<void> {\n const entry: StoredEntry<T> = {\n value,\n tags: options.tags ?? [],\n };\n const serialized = JSON.stringify(entry);\n const k = this.key(key);\n const ttlMs = options.ttl ?? this.defaultTTL;\n\n if (ttlMs) {\n await this.redis.set(k, serialized, 'PX', ttlMs);\n } else {\n await this.redis.set(k, serialized);\n }\n\n // Indexer la clé dans chaque tag (Redis Set pour invalidation rapide)\n if (options.tags?.length) {\n await Promise.all(\n options.tags.map(tag => this.redis.sadd(this.tagKey(tag), k))\n );\n }\n }\n\n async delete(key: string): Promise<void> {\n await this.redis.del(this.key(key));\n }\n\n async deleteByTag(tag: string): Promise<void> {\n // Script Lua atomique — évite les race conditions entre SMEMBERS et DEL\n await this.redis.eval(DELETE_BY_TAG_LUA, 1, this.tagKey(tag));\n }\n\n async deleteByPrefix(prefix: string): Promise<void> {\n // SCAN non-bloquant — ne jamais utiliser KEYS en production\n const fullPrefix = this.key(prefix);\n let cursor = '0';\n do {\n const [nextCursor, keys] = await this.redis.scan(\n cursor, 'MATCH', `${fullPrefix}*`, 'COUNT', 100\n );\n cursor = nextCursor;\n if (keys.length > 0) {\n await this.redis.del(...keys);\n }\n } while (cursor !== '0');\n }\n\n async clear(): Promise<void> {\n // SCAN + DELETE toutes les clés avec le préfixe Velix (non-bloquant)\n let cursor = '0';\n do {\n const [nextCursor, keys] = await this.redis.scan(\n cursor, 'MATCH', `${this.prefix}*`, 'COUNT', 100\n );\n cursor = nextCursor;\n if (keys.length > 0) await this.redis.del(...keys);\n } while (cursor !== '0');\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.redis.exists(this.key(key))) === 1;\n }\n\n /**\n * Ferme proprement les connexions Redis.\n * À appeler au shutdown de l'application.\n */\n async disconnect(): Promise<void> {\n await this.redis.quit();\n }\n\n /**\n * Expose l'instance Redis sous-jacente pour les cas avancés.\n */\n getClient(): Redis {\n return this.redis;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/redis.adapter.ts"],"names":[],"mappings":";;;AA6BA,IAAM,iBAAA,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA6BnB,IAAM,oBAAN,MAAiD;AAAA,EAC9C,KAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EAER,WAAA,CAAY,OAAA,GAAoC,EAAC,EAAG;AAClD,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,MAAA,IAAU,IAAI,KAAA,CAAM,OAAA,CAAQ,OAAO,wBAAwB,CAAA;AAChF,IAAA,IAAA,CAAK,MAAA,GAAS,QAAQ,SAAA,IAAa,QAAA;AACnC,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAAA,EAC5B;AAAA;AAAA,EAIQ,IAAI,CAAA,EAAmB;AAC7B,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,CAAA,CAAA;AAAA,EAC3B;AAAA,EAEQ,OAAO,GAAA,EAAqB;AAClC,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,MAAM,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA;AAAA,EACjC;AAAA;AAAA,EAIA,MAAM,IAAO,GAAA,EAAgC;AAC3C,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAC,CAAA;AAC9C,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC5B,MAAA,OAAO,KAAA,CAAM,KAAA;AAAA,IACf,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,GAAA,CAAO,GAAA,EAAa,KAAA,EAAU,OAAA,GAA2B,EAAC,EAAkB;AAChF,IAAA,MAAM,KAAA,GAAwB;AAAA,MAC5B,KAAA;AAAA,MACA,IAAA,EAAM,OAAA,CAAQ,IAAA,IAAQ;AAAC,KACzB;AACA,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACvC,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACtB,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,IAAO,IAAA,CAAK,UAAA;AAElC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,CAAA,EAAG,UAAA,EAAY,MAAM,KAAK,CAAA;AAAA,IACjD,CAAA,MAAO;AACL,MAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,CAAA,EAAG,UAAU,CAAA;AAAA,IACpC;AAGA,IAAA,IAAI,OAAA,CAAQ,MAAM,MAAA,EAAQ;AACxB,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,QAAA,EAAS;AACrC,MAAA,KAAA,MAAW,GAAA,IAAO,QAAQ,IAAA,EAAM;AAC9B,QAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,CAAO,GAAG,CAAA;AAC5B,QAAA,QAAA,CAAS,IAAA,CAAK,MAAM,CAAC,CAAA;AACrB,QAAA,IAAI,KAAA,EAAO;AACT,UAAA,QAAA,CAAS,OAAA,CAAQ,IAAA,EAAM,KAAA,GAAQ,CAAC,CAAA;AAAA,QAClC;AAAA,MACF;AACA,MAAA,MAAM,SAAS,IAAA,EAAK;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,EACpC;AAAA,EAEA,MAAM,YAAY,GAAA,EAA4B;AAE5C,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,iBAAA,EAAmB,GAAG,IAAA,CAAK,MAAA,CAAO,GAAG,CAAC,CAAA;AAAA,EAC9D;AAAA,EAEA,MAAM,eAAe,MAAA,EAA+B;AAElD,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAClC,IAAA,IAAI,MAAA,GAAS,GAAA;AACb,IAAA,GAAG;AACD,MAAA,MAAM,CAAC,UAAA,EAAY,IAAI,CAAA,GAAI,MAAM,KAAK,KAAA,CAAM,IAAA;AAAA,QAC1C,MAAA;AAAA,QAAQ,OAAA;AAAA,QAAS,GAAG,UAAU,CAAA,CAAA,CAAA;AAAA,QAAK,OAAA;AAAA,QAAS;AAAA,OAC9C;AACA,MAAA,MAAA,GAAS,UAAA;AACT,MAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,MAC9B;AAAA,IACF,SAAS,MAAA,KAAW,GAAA;AAAA,EACtB;AAAA,EAEA,MAAM,KAAA,GAAuB;AAE3B,IAAA,IAAI,MAAA,GAAS,GAAA;AACb,IAAA,GAAG;AACD,MAAA,MAAM,CAAC,UAAA,EAAY,IAAI,CAAA,GAAI,MAAM,KAAK,KAAA,CAAM,IAAA;AAAA,QAC1C,MAAA;AAAA,QAAQ,OAAA;AAAA,QAAS,CAAA,EAAG,KAAK,MAAM,CAAA,CAAA,CAAA;AAAA,QAAK,OAAA;AAAA,QAAS;AAAA,OAC/C;AACA,MAAA,MAAA,GAAS,UAAA;AACT,MAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG,MAAM,KAAK,KAAA,CAAM,GAAA,CAAI,GAAG,IAAI,CAAA;AAAA,IACnD,SAAS,MAAA,KAAW,GAAA;AAAA,EACtB;AAAA,EAEA,MAAM,IAAI,GAAA,EAA+B;AACvC,IAAA,OAAQ,MAAM,KAAK,KAAA,CAAM,MAAA,CAAO,KAAK,GAAA,CAAI,GAAG,CAAC,CAAA,KAAO,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAA,GAA4B;AAChC,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,EAAK;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAA,GAAmB;AACjB,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF","file":"index.js","sourcesContent":["import Redis from 'ioredis';\nimport type { RedisCacheAdapterOptions, StoredEntry } from './types.js';\n\n// ============================================================================\n// ICacheAdapter — inline pour éviter la dépendance circulaire et permettre\n// à @velix/cache-redis d'être utilisable sans velix-core installé\n// ============================================================================\n\nexport interface ICacheAdapter {\n get<T>(key: string): Promise<T | null>;\n set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;\n delete(key: string): Promise<void>;\n deleteByTag(tag: string): Promise<void>;\n deleteByPrefix(prefix: string): Promise<void>;\n clear(): Promise<void>;\n has(key: string): Promise<boolean>;\n}\n\nexport type CacheSetOptions = {\n /** TTL en millisecondes */\n ttl?: number;\n /** Tags pour l'invalidation groupée */\n tags?: string[];\n};\n\n// ============================================================================\n// Script Lua inline pour deleteByTag — atomicité garantie\n// ============================================================================\n\nconst DELETE_BY_TAG_LUA = `\nlocal keys = redis.call('SMEMBERS', KEYS[1])\nif #keys > 0 then\n redis.call('DEL', unpack(keys))\nend\nredis.call('DEL', KEYS[1])\nreturn #keys\n`;\n\n// ============================================================================\n// RedisCacheAdapter\n// ============================================================================\n\n/**\n * Adaptateur de cache Redis pour Velix.\n * Implémente ICacheAdapter via ioredis.\n *\n * @example\n * ```ts\n * import { RedisCacheAdapter } from '@velix/cache-redis';\n * import { defineConfig } from 'velix';\n *\n * export default defineConfig({\n * cache: {\n * adapter: new RedisCacheAdapter({ url: process.env.REDIS_URL }),\n * }\n * });\n * ```\n */\nexport class RedisCacheAdapter implements ICacheAdapter {\n private redis: Redis;\n private prefix: string;\n private defaultTTL?: number;\n\n constructor(options: RedisCacheAdapterOptions = {}) {\n this.redis = options.client ?? new Redis(options.url ?? 'redis://localhost:6379');\n this.prefix = options.keyPrefix ?? 'velix:';\n this.defaultTTL = options.defaultTTL;\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────────\n\n private key(k: string): string {\n return `${this.prefix}${k}`;\n }\n\n private tagKey(tag: string): string {\n return `${this.prefix}tag:${tag}`;\n }\n\n // ── ICacheAdapter ─────────────────────────────────────────────────────────\n\n async get<T>(key: string): Promise<T | null> {\n const raw = await this.redis.get(this.key(key));\n if (!raw) return null;\n try {\n const entry = JSON.parse(raw) as StoredEntry<T>;\n return entry.value;\n } catch {\n return null;\n }\n }\n\n async set<T>(key: string, value: T, options: CacheSetOptions = {}): Promise<void> {\n const entry: StoredEntry<T> = {\n value,\n tags: options.tags ?? [],\n };\n const serialized = JSON.stringify(entry);\n const k = this.key(key);\n const ttlMs = options.ttl ?? this.defaultTTL;\n\n if (ttlMs) {\n await this.redis.set(k, serialized, 'PX', ttlMs);\n } else {\n await this.redis.set(k, serialized);\n }\n\n // Indexer la clé dans chaque tag (Redis Set pour invalidation rapide)\n if (options.tags?.length) {\n const pipeline = this.redis.pipeline();\n for (const tag of options.tags) {\n const tKey = this.tagKey(tag);\n pipeline.sadd(tKey, k);\n if (ttlMs) {\n pipeline.pexpire(tKey, ttlMs * 2); // Expire tag set slightly after items\n }\n }\n await pipeline.exec();\n }\n }\n\n async delete(key: string): Promise<void> {\n await this.redis.del(this.key(key));\n }\n\n async deleteByTag(tag: string): Promise<void> {\n // Script Lua atomique — évite les race conditions entre SMEMBERS et DEL\n await this.redis.eval(DELETE_BY_TAG_LUA, 1, this.tagKey(tag));\n }\n\n async deleteByPrefix(prefix: string): Promise<void> {\n // SCAN non-bloquant — ne jamais utiliser KEYS en production\n const fullPrefix = this.key(prefix);\n let cursor = '0';\n do {\n const [nextCursor, keys] = await this.redis.scan(\n cursor, 'MATCH', `${fullPrefix}*`, 'COUNT', 100\n );\n cursor = nextCursor;\n if (keys.length > 0) {\n await this.redis.del(...keys);\n }\n } while (cursor !== '0');\n }\n\n async clear(): Promise<void> {\n // SCAN + DELETE toutes les clés avec le préfixe Velix (non-bloquant)\n let cursor = '0';\n do {\n const [nextCursor, keys] = await this.redis.scan(\n cursor, 'MATCH', `${this.prefix}*`, 'COUNT', 100\n );\n cursor = nextCursor;\n if (keys.length > 0) await this.redis.del(...keys);\n } while (cursor !== '0');\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.redis.exists(this.key(key))) === 1;\n }\n\n /**\n * Ferme proprement les connexions Redis.\n * À appeler au shutdown de l'application.\n */\n async disconnect(): Promise<void> {\n await this.redis.quit();\n }\n\n /**\n * Expose l'instance Redis sous-jacente pour les cas avancés.\n */\n getClient(): Redis {\n return this.redis;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvelix/cache-redis",
3
- "version": "5.3.5",
3
+ "version": "5.3.6",
4
4
  "description": "Redis cache adapter for Velix — implements ICacheAdapter via ioredis",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",