veryfront 0.1.217 → 0.1.218
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/esm/deno.d.ts +1 -2
- package/esm/deno.js +6 -4
- package/esm/src/extensions/interfaces/auth-provider.d.ts +30 -3
- package/esm/src/extensions/interfaces/auth-provider.d.ts.map +1 -1
- package/esm/src/extensions/interfaces/index.d.ts +2 -1
- package/esm/src/extensions/interfaces/index.d.ts.map +1 -1
- package/esm/src/extensions/interfaces/token-cache-store.d.ts +56 -0
- package/esm/src/extensions/interfaces/token-cache-store.d.ts.map +1 -0
- package/esm/src/extensions/interfaces/token-cache-store.js +12 -0
- package/esm/src/extensions/recommendations.d.ts.map +1 -1
- package/esm/src/extensions/recommendations.js +1 -0
- package/esm/src/proxy/cache/index.d.ts +1 -1
- package/esm/src/proxy/cache/index.d.ts.map +1 -1
- package/esm/src/proxy/cache/index.js +25 -15
- package/esm/src/proxy/cache/tracing-cache.d.ts +31 -0
- package/esm/src/proxy/cache/tracing-cache.d.ts.map +1 -0
- package/esm/src/proxy/cache/tracing-cache.js +44 -0
- package/esm/src/proxy/cache/types.d.ts +1 -1
- package/esm/src/proxy/cache/types.js +1 -1
- package/esm/src/proxy/handler.d.ts +7 -0
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +50 -29
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +66 -35
- package/src/deno.js +6 -4
- package/src/src/extensions/interfaces/auth-provider.ts +35 -3
- package/src/src/extensions/interfaces/index.ts +10 -1
- package/src/src/extensions/interfaces/token-cache-store.ts +58 -0
- package/src/src/extensions/recommendations.ts +1 -0
- package/src/src/proxy/cache/index.ts +27 -15
- package/src/src/proxy/cache/tracing-cache.ts +77 -0
- package/src/src/proxy/cache/types.ts +1 -1
- package/src/src/proxy/handler.ts +57 -31
- package/src/src/utils/version-constant.ts +1 -1
- package/esm/src/proxy/cache/redis-cache.d.ts +0 -25
- package/esm/src/proxy/cache/redis-cache.d.ts.map +0 -1
- package/esm/src/proxy/cache/redis-cache.js +0 -219
- package/src/src/proxy/cache/redis-cache.ts +0 -255
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { CacheStats, RedisCacheOptions, TokenCache, TokenCacheEntry } from "./types.js";
|
|
2
|
-
export declare class RedisCache implements TokenCache {
|
|
3
|
-
private client;
|
|
4
|
-
private readonly prefix;
|
|
5
|
-
private readonly url;
|
|
6
|
-
private readonly connectTimeout;
|
|
7
|
-
private readonly tls;
|
|
8
|
-
private readonly password?;
|
|
9
|
-
private readonly username?;
|
|
10
|
-
private hits;
|
|
11
|
-
private misses;
|
|
12
|
-
private connected;
|
|
13
|
-
constructor(options: RedisCacheOptions);
|
|
14
|
-
private key;
|
|
15
|
-
get(key: string): Promise<TokenCacheEntry | null>;
|
|
16
|
-
set(key: string, entry: TokenCacheEntry): Promise<void>;
|
|
17
|
-
delete(key: string): Promise<void>;
|
|
18
|
-
clear(): Promise<void>;
|
|
19
|
-
has(key: string): Promise<boolean>;
|
|
20
|
-
stats(): Promise<CacheStats>;
|
|
21
|
-
close(): Promise<void>;
|
|
22
|
-
private getConnectedClient;
|
|
23
|
-
private ensureConnected;
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=redis-cache.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/proxy/cache/redis-cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAY7F,qBAAa,UAAW,YAAW,UAAU;IAC3C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAU;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAS;gBAEd,OAAO,EAAE,iBAAiB;IAStC,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAoCjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBvD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBlC,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IAkB5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAmBd,kBAAkB;YAQlB,eAAe;CAoC9B"}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { createClient } from "redis";
|
|
2
|
-
import { withSpan } from "../tracing.js";
|
|
3
|
-
import { proxyLogger } from "../logger.js";
|
|
4
|
-
const logger = proxyLogger.child({ module: "redis-cache" });
|
|
5
|
-
const DEFAULT_PREFIX = "vf:token:";
|
|
6
|
-
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
7
|
-
const DEFAULT_SCAN_COUNT = 100;
|
|
8
|
-
const MAX_RECONNECT_RETRIES = 3;
|
|
9
|
-
const RECONNECT_BACKOFF_BASE_MS = 100;
|
|
10
|
-
const RECONNECT_BACKOFF_MAX_MS = 3_000;
|
|
11
|
-
export class RedisCache {
|
|
12
|
-
client = null;
|
|
13
|
-
prefix;
|
|
14
|
-
url;
|
|
15
|
-
connectTimeout;
|
|
16
|
-
tls;
|
|
17
|
-
password;
|
|
18
|
-
username;
|
|
19
|
-
hits = 0;
|
|
20
|
-
misses = 0;
|
|
21
|
-
connected = false;
|
|
22
|
-
constructor(options) {
|
|
23
|
-
this.url = options.url;
|
|
24
|
-
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
25
|
-
this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
26
|
-
this.tls = options.tls ?? options.url.startsWith("rediss://");
|
|
27
|
-
this.password = options.password;
|
|
28
|
-
this.username = options.username;
|
|
29
|
-
}
|
|
30
|
-
key(k) {
|
|
31
|
-
return `${this.prefix}${k}`;
|
|
32
|
-
}
|
|
33
|
-
async get(key) {
|
|
34
|
-
return withSpan("cache.redis.get", async () => {
|
|
35
|
-
try {
|
|
36
|
-
const client = await this.getConnectedClient();
|
|
37
|
-
const data = await client.get(this.key(key));
|
|
38
|
-
if (!data) {
|
|
39
|
-
this.misses++;
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
const entry = JSON.parse(data);
|
|
43
|
-
if (Date.now() >= entry.expiresAt) {
|
|
44
|
-
await client.del(this.key(key));
|
|
45
|
-
this.misses++;
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
this.hits++;
|
|
49
|
-
return entry;
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
logger.error("[RedisCache] Get error", {
|
|
53
|
-
error: error instanceof Error ? error.message : String(error),
|
|
54
|
-
});
|
|
55
|
-
this.connected = false;
|
|
56
|
-
this.misses++;
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
}, { "cache.key": key });
|
|
60
|
-
}
|
|
61
|
-
async set(key, entry) {
|
|
62
|
-
return withSpan("cache.redis.set", async () => {
|
|
63
|
-
try {
|
|
64
|
-
const client = await this.getConnectedClient();
|
|
65
|
-
const ttlMs = entry.expiresAt - Date.now();
|
|
66
|
-
const ttlSeconds = Math.max(1, Math.floor(ttlMs / 1000));
|
|
67
|
-
await client.setEx(this.key(key), ttlSeconds, JSON.stringify(entry));
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
logger.error("[RedisCache] Set error", {
|
|
71
|
-
error: error instanceof Error ? error.message : String(error),
|
|
72
|
-
});
|
|
73
|
-
this.connected = false;
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
}, { "cache.key": key });
|
|
77
|
-
}
|
|
78
|
-
async delete(key) {
|
|
79
|
-
return withSpan("cache.redis.delete", async () => {
|
|
80
|
-
try {
|
|
81
|
-
const client = await this.getConnectedClient();
|
|
82
|
-
await client.del(this.key(key));
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
logger.error("[RedisCache] Delete error", {
|
|
86
|
-
error: error instanceof Error ? error.message : String(error),
|
|
87
|
-
});
|
|
88
|
-
this.connected = false;
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}, { "cache.key": key });
|
|
92
|
-
}
|
|
93
|
-
async clear() {
|
|
94
|
-
return withSpan("cache.redis.clear", async () => {
|
|
95
|
-
try {
|
|
96
|
-
const client = await this.getConnectedClient();
|
|
97
|
-
const pattern = `${this.prefix}*`;
|
|
98
|
-
// redis v5: scan cursor is string-based to prevent Number.MAX_SAFE_INTEGER overflow
|
|
99
|
-
let cursor = "0";
|
|
100
|
-
let totalDeleted = 0;
|
|
101
|
-
do {
|
|
102
|
-
// deno-lint-ignore no-explicit-any
|
|
103
|
-
const result = await client.scan(cursor, {
|
|
104
|
-
MATCH: pattern,
|
|
105
|
-
COUNT: DEFAULT_SCAN_COUNT,
|
|
106
|
-
});
|
|
107
|
-
cursor = String(result.cursor);
|
|
108
|
-
if (result.keys.length > 0) {
|
|
109
|
-
totalDeleted += await client.del(result.keys);
|
|
110
|
-
}
|
|
111
|
-
} while (cursor !== "0");
|
|
112
|
-
if (totalDeleted > 0) {
|
|
113
|
-
logger.info(`[RedisCache] Cleared ${totalDeleted} keys`);
|
|
114
|
-
}
|
|
115
|
-
this.hits = 0;
|
|
116
|
-
this.misses = 0;
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
logger.error("[RedisCache] Clear error", {
|
|
120
|
-
error: error instanceof Error ? error.message : String(error),
|
|
121
|
-
});
|
|
122
|
-
this.connected = false;
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
async has(key) {
|
|
128
|
-
return withSpan("cache.redis.has", async () => {
|
|
129
|
-
try {
|
|
130
|
-
const client = await this.getConnectedClient();
|
|
131
|
-
return (await client.exists(this.key(key))) === 1;
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
logger.error("[RedisCache] Has error", {
|
|
135
|
-
error: error instanceof Error ? error.message : String(error),
|
|
136
|
-
});
|
|
137
|
-
this.connected = false;
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
}, { "cache.key": key });
|
|
141
|
-
}
|
|
142
|
-
async stats() {
|
|
143
|
-
return withSpan("cache.redis.stats", async () => {
|
|
144
|
-
let size = 0;
|
|
145
|
-
try {
|
|
146
|
-
const client = await this.getConnectedClient();
|
|
147
|
-
size = await client.dbSize();
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
this.connected = false;
|
|
151
|
-
logger.error("[RedisCache] Stats error", {
|
|
152
|
-
error: error instanceof Error ? error.message : String(error),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return { hits: this.hits, misses: this.misses, size, type: "redis" };
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
async close() {
|
|
159
|
-
return withSpan("cache.redis.close", async () => {
|
|
160
|
-
const client = this.client;
|
|
161
|
-
if (!client) {
|
|
162
|
-
this.connected = false;
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
await client.close();
|
|
167
|
-
}
|
|
168
|
-
catch (_) {
|
|
169
|
-
// expected: close errors are non-critical
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
this.client = null;
|
|
173
|
-
this.connected = false;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
async getConnectedClient() {
|
|
178
|
-
await this.ensureConnected();
|
|
179
|
-
if (!this.client) {
|
|
180
|
-
throw new Error("Redis client not available after connect");
|
|
181
|
-
}
|
|
182
|
-
return this.client;
|
|
183
|
-
}
|
|
184
|
-
async ensureConnected() {
|
|
185
|
-
return withSpan("cache.redis.connect", async () => {
|
|
186
|
-
if (this.connected && this.client)
|
|
187
|
-
return;
|
|
188
|
-
// deno-lint-ignore no-explicit-any
|
|
189
|
-
const clientOpts = {
|
|
190
|
-
url: this.url,
|
|
191
|
-
socket: {
|
|
192
|
-
connectTimeout: this.connectTimeout,
|
|
193
|
-
tls: this.tls || undefined,
|
|
194
|
-
reconnectStrategy: (retries) => {
|
|
195
|
-
if (retries > MAX_RECONNECT_RETRIES) {
|
|
196
|
-
return new Error("Max reconnection attempts reached");
|
|
197
|
-
}
|
|
198
|
-
return Math.min(retries * RECONNECT_BACKOFF_BASE_MS, RECONNECT_BACKOFF_MAX_MS);
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
if (this.password)
|
|
203
|
-
clientOpts.password = this.password;
|
|
204
|
-
if (this.username)
|
|
205
|
-
clientOpts.username = this.username;
|
|
206
|
-
const client = createClient(clientOpts);
|
|
207
|
-
client.on("error", (err) => {
|
|
208
|
-
logger.error("[RedisCache] Client error", {
|
|
209
|
-
error: err instanceof Error ? err.message : String(err),
|
|
210
|
-
});
|
|
211
|
-
this.connected = false;
|
|
212
|
-
});
|
|
213
|
-
this.client = client;
|
|
214
|
-
await client.connect();
|
|
215
|
-
this.connected = true;
|
|
216
|
-
logger.info("[RedisCache] Connected");
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { createClient, type RedisClientType } from "redis";
|
|
2
|
-
import type { CacheStats, RedisCacheOptions, TokenCache, TokenCacheEntry } from "./types.js";
|
|
3
|
-
import { withSpan } from "../tracing.js";
|
|
4
|
-
import { proxyLogger } from "../logger.js";
|
|
5
|
-
|
|
6
|
-
const logger = proxyLogger.child({ module: "redis-cache" });
|
|
7
|
-
const DEFAULT_PREFIX = "vf:token:";
|
|
8
|
-
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
9
|
-
const DEFAULT_SCAN_COUNT = 100;
|
|
10
|
-
const MAX_RECONNECT_RETRIES = 3;
|
|
11
|
-
const RECONNECT_BACKOFF_BASE_MS = 100;
|
|
12
|
-
const RECONNECT_BACKOFF_MAX_MS = 3_000;
|
|
13
|
-
|
|
14
|
-
export class RedisCache implements TokenCache {
|
|
15
|
-
private client: RedisClientType | null = null;
|
|
16
|
-
private readonly prefix: string;
|
|
17
|
-
private readonly url: string;
|
|
18
|
-
private readonly connectTimeout: number;
|
|
19
|
-
private readonly tls: boolean;
|
|
20
|
-
private readonly password?: string;
|
|
21
|
-
private readonly username?: string;
|
|
22
|
-
private hits = 0;
|
|
23
|
-
private misses = 0;
|
|
24
|
-
private connected = false;
|
|
25
|
-
|
|
26
|
-
constructor(options: RedisCacheOptions) {
|
|
27
|
-
this.url = options.url;
|
|
28
|
-
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
29
|
-
this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
30
|
-
this.tls = options.tls ?? options.url.startsWith("rediss://");
|
|
31
|
-
this.password = options.password;
|
|
32
|
-
this.username = options.username;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private key(k: string): string {
|
|
36
|
-
return `${this.prefix}${k}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async get(key: string): Promise<TokenCacheEntry | null> {
|
|
40
|
-
return withSpan(
|
|
41
|
-
"cache.redis.get",
|
|
42
|
-
async () => {
|
|
43
|
-
try {
|
|
44
|
-
const client = await this.getConnectedClient();
|
|
45
|
-
const data = await client.get(this.key(key));
|
|
46
|
-
|
|
47
|
-
if (!data) {
|
|
48
|
-
this.misses++;
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const entry = JSON.parse(data) as TokenCacheEntry;
|
|
53
|
-
|
|
54
|
-
if (Date.now() >= entry.expiresAt) {
|
|
55
|
-
await client.del(this.key(key));
|
|
56
|
-
this.misses++;
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
this.hits++;
|
|
61
|
-
return entry;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
logger.error("[RedisCache] Get error", {
|
|
64
|
-
error: error instanceof Error ? error.message : String(error),
|
|
65
|
-
});
|
|
66
|
-
this.connected = false;
|
|
67
|
-
this.misses++;
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{ "cache.key": key },
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async set(key: string, entry: TokenCacheEntry): Promise<void> {
|
|
76
|
-
return withSpan(
|
|
77
|
-
"cache.redis.set",
|
|
78
|
-
async () => {
|
|
79
|
-
try {
|
|
80
|
-
const client = await this.getConnectedClient();
|
|
81
|
-
const ttlMs = entry.expiresAt - Date.now();
|
|
82
|
-
const ttlSeconds = Math.max(1, Math.floor(ttlMs / 1000));
|
|
83
|
-
await client.setEx(this.key(key), ttlSeconds, JSON.stringify(entry));
|
|
84
|
-
} catch (error) {
|
|
85
|
-
logger.error("[RedisCache] Set error", {
|
|
86
|
-
error: error instanceof Error ? error.message : String(error),
|
|
87
|
-
});
|
|
88
|
-
this.connected = false;
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
{ "cache.key": key },
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async delete(key: string): Promise<void> {
|
|
97
|
-
return withSpan(
|
|
98
|
-
"cache.redis.delete",
|
|
99
|
-
async () => {
|
|
100
|
-
try {
|
|
101
|
-
const client = await this.getConnectedClient();
|
|
102
|
-
await client.del(this.key(key));
|
|
103
|
-
} catch (error) {
|
|
104
|
-
logger.error("[RedisCache] Delete error", {
|
|
105
|
-
error: error instanceof Error ? error.message : String(error),
|
|
106
|
-
});
|
|
107
|
-
this.connected = false;
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
{ "cache.key": key },
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async clear(): Promise<void> {
|
|
116
|
-
return withSpan("cache.redis.clear", async () => {
|
|
117
|
-
try {
|
|
118
|
-
const client = await this.getConnectedClient();
|
|
119
|
-
|
|
120
|
-
const pattern = `${this.prefix}*`;
|
|
121
|
-
// redis v5: scan cursor is string-based to prevent Number.MAX_SAFE_INTEGER overflow
|
|
122
|
-
let cursor = "0";
|
|
123
|
-
let totalDeleted = 0;
|
|
124
|
-
|
|
125
|
-
do {
|
|
126
|
-
// deno-lint-ignore no-explicit-any
|
|
127
|
-
const result = await (client as any).scan(cursor, {
|
|
128
|
-
MATCH: pattern,
|
|
129
|
-
COUNT: DEFAULT_SCAN_COUNT,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
cursor = String(result.cursor);
|
|
133
|
-
|
|
134
|
-
if (result.keys.length > 0) {
|
|
135
|
-
totalDeleted += await client.del(result.keys);
|
|
136
|
-
}
|
|
137
|
-
} while (cursor !== "0");
|
|
138
|
-
|
|
139
|
-
if (totalDeleted > 0) {
|
|
140
|
-
logger.info(`[RedisCache] Cleared ${totalDeleted} keys`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
this.hits = 0;
|
|
144
|
-
this.misses = 0;
|
|
145
|
-
} catch (error) {
|
|
146
|
-
logger.error("[RedisCache] Clear error", {
|
|
147
|
-
error: error instanceof Error ? error.message : String(error),
|
|
148
|
-
});
|
|
149
|
-
this.connected = false;
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async has(key: string): Promise<boolean> {
|
|
156
|
-
return withSpan(
|
|
157
|
-
"cache.redis.has",
|
|
158
|
-
async () => {
|
|
159
|
-
try {
|
|
160
|
-
const client = await this.getConnectedClient();
|
|
161
|
-
return (await client.exists(this.key(key))) === 1;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
logger.error("[RedisCache] Has error", {
|
|
164
|
-
error: error instanceof Error ? error.message : String(error),
|
|
165
|
-
});
|
|
166
|
-
this.connected = false;
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
{ "cache.key": key },
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async stats(): Promise<CacheStats> {
|
|
175
|
-
return withSpan("cache.redis.stats", async () => {
|
|
176
|
-
let size = 0;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const client = await this.getConnectedClient();
|
|
180
|
-
size = await client.dbSize();
|
|
181
|
-
} catch (error) {
|
|
182
|
-
this.connected = false;
|
|
183
|
-
logger.error("[RedisCache] Stats error", {
|
|
184
|
-
error: error instanceof Error ? error.message : String(error),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { hits: this.hits, misses: this.misses, size, type: "redis" as const };
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async close(): Promise<void> {
|
|
193
|
-
return withSpan("cache.redis.close", async () => {
|
|
194
|
-
const client = this.client;
|
|
195
|
-
if (!client) {
|
|
196
|
-
this.connected = false;
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
await client.close();
|
|
202
|
-
} catch (_) {
|
|
203
|
-
// expected: close errors are non-critical
|
|
204
|
-
} finally {
|
|
205
|
-
this.client = null;
|
|
206
|
-
this.connected = false;
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private async getConnectedClient(): Promise<RedisClientType> {
|
|
212
|
-
await this.ensureConnected();
|
|
213
|
-
if (!this.client) {
|
|
214
|
-
throw new Error("Redis client not available after connect");
|
|
215
|
-
}
|
|
216
|
-
return this.client;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private async ensureConnected(): Promise<void> {
|
|
220
|
-
return withSpan("cache.redis.connect", async () => {
|
|
221
|
-
if (this.connected && this.client) return;
|
|
222
|
-
|
|
223
|
-
// deno-lint-ignore no-explicit-any
|
|
224
|
-
const clientOpts: Record<string, any> = {
|
|
225
|
-
url: this.url,
|
|
226
|
-
socket: {
|
|
227
|
-
connectTimeout: this.connectTimeout,
|
|
228
|
-
tls: this.tls || undefined,
|
|
229
|
-
reconnectStrategy: (retries: number) => {
|
|
230
|
-
if (retries > MAX_RECONNECT_RETRIES) {
|
|
231
|
-
return new Error("Max reconnection attempts reached");
|
|
232
|
-
}
|
|
233
|
-
return Math.min(retries * RECONNECT_BACKOFF_BASE_MS, RECONNECT_BACKOFF_MAX_MS);
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
if (this.password) clientOpts.password = this.password;
|
|
238
|
-
if (this.username) clientOpts.username = this.username;
|
|
239
|
-
|
|
240
|
-
const client = createClient(clientOpts);
|
|
241
|
-
|
|
242
|
-
client.on("error", (err) => {
|
|
243
|
-
logger.error("[RedisCache] Client error", {
|
|
244
|
-
error: err instanceof Error ? err.message : String(err),
|
|
245
|
-
});
|
|
246
|
-
this.connected = false;
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
this.client = client as RedisClientType;
|
|
250
|
-
await client.connect();
|
|
251
|
-
this.connected = true;
|
|
252
|
-
logger.info("[RedisCache] Connected");
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|