@teamvelix/cache-redis 0.1.0 → 5.3.5

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Velix Team
3
+ Copyright (c) 2026-2030 Velix Team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,80 @@
1
+ import Redis from 'ioredis';
2
+
3
+ /**
4
+ * Options de configuration pour RedisCacheAdapter
5
+ */
6
+ type RedisCacheAdapterOptions = {
7
+ /** URL de connexion Redis. Ex: 'redis://localhost:6379' */
8
+ url?: string;
9
+ /** Instance ioredis pré-configurée (prioritaire sur `url`) */
10
+ client?: Redis;
11
+ /** Préfixe pour toutes les clés Velix dans Redis. Défaut: 'velix:' */
12
+ keyPrefix?: string;
13
+ /** TTL par défaut en millisecondes. Défaut: undefined (pas d'expiration) */
14
+ defaultTTL?: number;
15
+ };
16
+ /**
17
+ * Structure interne stockée dans Redis (sérialisée en JSON)
18
+ */
19
+ type StoredEntry<T> = {
20
+ value: T;
21
+ tags: string[];
22
+ };
23
+
24
+ interface ICacheAdapter {
25
+ get<T>(key: string): Promise<T | null>;
26
+ set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
27
+ delete(key: string): Promise<void>;
28
+ deleteByTag(tag: string): Promise<void>;
29
+ deleteByPrefix(prefix: string): Promise<void>;
30
+ clear(): Promise<void>;
31
+ has(key: string): Promise<boolean>;
32
+ }
33
+ type CacheSetOptions = {
34
+ /** TTL en millisecondes */
35
+ ttl?: number;
36
+ /** Tags pour l'invalidation groupée */
37
+ tags?: string[];
38
+ };
39
+ /**
40
+ * Adaptateur de cache Redis pour Velix.
41
+ * Implémente ICacheAdapter via ioredis.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { RedisCacheAdapter } from '@velix/cache-redis';
46
+ * import { defineConfig } from 'velix';
47
+ *
48
+ * export default defineConfig({
49
+ * cache: {
50
+ * adapter: new RedisCacheAdapter({ url: process.env.REDIS_URL }),
51
+ * }
52
+ * });
53
+ * ```
54
+ */
55
+ declare class RedisCacheAdapter implements ICacheAdapter {
56
+ private redis;
57
+ private prefix;
58
+ private defaultTTL?;
59
+ constructor(options?: RedisCacheAdapterOptions);
60
+ private key;
61
+ private tagKey;
62
+ get<T>(key: string): Promise<T | null>;
63
+ set<T>(key: string, value: T, options?: CacheSetOptions): Promise<void>;
64
+ delete(key: string): Promise<void>;
65
+ deleteByTag(tag: string): Promise<void>;
66
+ deleteByPrefix(prefix: string): Promise<void>;
67
+ clear(): Promise<void>;
68
+ has(key: string): Promise<boolean>;
69
+ /**
70
+ * Ferme proprement les connexions Redis.
71
+ * À appeler au shutdown de l'application.
72
+ */
73
+ disconnect(): Promise<void>;
74
+ /**
75
+ * Expose l'instance Redis sous-jacente pour les cas avancés.
76
+ */
77
+ getClient(): Redis;
78
+ }
79
+
80
+ export { type CacheSetOptions, type ICacheAdapter, RedisCacheAdapter, type RedisCacheAdapterOptions, type StoredEntry };
package/dist/index.js ADDED
@@ -0,0 +1,115 @@
1
+ import Redis from 'ioredis';
2
+
3
+ // src/redis.adapter.ts
4
+ var DELETE_BY_TAG_LUA = `
5
+ local keys = redis.call('SMEMBERS', KEYS[1])
6
+ if #keys > 0 then
7
+ redis.call('DEL', unpack(keys))
8
+ end
9
+ redis.call('DEL', KEYS[1])
10
+ return #keys
11
+ `;
12
+ var RedisCacheAdapter = class {
13
+ redis;
14
+ prefix;
15
+ defaultTTL;
16
+ constructor(options = {}) {
17
+ this.redis = options.client ?? new Redis(options.url ?? "redis://localhost:6379");
18
+ this.prefix = options.keyPrefix ?? "velix:";
19
+ this.defaultTTL = options.defaultTTL;
20
+ }
21
+ // ── Helpers ──────────────────────────────────────────────────────────────
22
+ key(k) {
23
+ return `${this.prefix}${k}`;
24
+ }
25
+ tagKey(tag) {
26
+ return `${this.prefix}tag:${tag}`;
27
+ }
28
+ // ── ICacheAdapter ─────────────────────────────────────────────────────────
29
+ async get(key) {
30
+ const raw = await this.redis.get(this.key(key));
31
+ if (!raw) return null;
32
+ try {
33
+ const entry = JSON.parse(raw);
34
+ return entry.value;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ async set(key, value, options = {}) {
40
+ const entry = {
41
+ value,
42
+ tags: options.tags ?? []
43
+ };
44
+ const serialized = JSON.stringify(entry);
45
+ const k = this.key(key);
46
+ const ttlMs = options.ttl ?? this.defaultTTL;
47
+ if (ttlMs) {
48
+ await this.redis.set(k, serialized, "PX", ttlMs);
49
+ } else {
50
+ await this.redis.set(k, serialized);
51
+ }
52
+ if (options.tags?.length) {
53
+ await Promise.all(
54
+ options.tags.map((tag) => this.redis.sadd(this.tagKey(tag), k))
55
+ );
56
+ }
57
+ }
58
+ async delete(key) {
59
+ await this.redis.del(this.key(key));
60
+ }
61
+ async deleteByTag(tag) {
62
+ await this.redis.eval(DELETE_BY_TAG_LUA, 1, this.tagKey(tag));
63
+ }
64
+ async deleteByPrefix(prefix) {
65
+ const fullPrefix = this.key(prefix);
66
+ let cursor = "0";
67
+ do {
68
+ const [nextCursor, keys] = await this.redis.scan(
69
+ cursor,
70
+ "MATCH",
71
+ `${fullPrefix}*`,
72
+ "COUNT",
73
+ 100
74
+ );
75
+ cursor = nextCursor;
76
+ if (keys.length > 0) {
77
+ await this.redis.del(...keys);
78
+ }
79
+ } while (cursor !== "0");
80
+ }
81
+ async clear() {
82
+ let cursor = "0";
83
+ do {
84
+ const [nextCursor, keys] = await this.redis.scan(
85
+ cursor,
86
+ "MATCH",
87
+ `${this.prefix}*`,
88
+ "COUNT",
89
+ 100
90
+ );
91
+ cursor = nextCursor;
92
+ if (keys.length > 0) await this.redis.del(...keys);
93
+ } while (cursor !== "0");
94
+ }
95
+ async has(key) {
96
+ return await this.redis.exists(this.key(key)) === 1;
97
+ }
98
+ /**
99
+ * Ferme proprement les connexions Redis.
100
+ * À appeler au shutdown de l'application.
101
+ */
102
+ async disconnect() {
103
+ await this.redis.quit();
104
+ }
105
+ /**
106
+ * Expose l'instance Redis sous-jacente pour les cas avancés.
107
+ */
108
+ getClient() {
109
+ return this.redis;
110
+ }
111
+ };
112
+
113
+ export { RedisCacheAdapter };
114
+ //# sourceMappingURL=index.js.map
115
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvelix/cache-redis",
3
- "version": "0.1.0",
3
+ "version": "5.3.5",
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",