@beignet/provider-redis 0.0.3 → 0.0.4
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/CHANGELOG.md +38 -0
- package/README.md +58 -16
- package/dist/index.d.ts +93 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +307 -223
- package/dist/index.js.map +1 -1
- package/package.json +38 -3
- package/src/index.ts +396 -245
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* Configuration:
|
|
7
7
|
* - REDIS_URL: Redis connection URL (required)
|
|
8
8
|
* - REDIS_DB: Redis database number (optional, default: 0)
|
|
9
|
+
* - REDIS_CONNECT_TIMEOUT_MS: Initial connection timeout in milliseconds (optional, default: 5000)
|
|
10
|
+
* - REDIS_MAX_RETRIES_PER_REQUEST: Per-command retry limit (optional, default: 2)
|
|
11
|
+
* - REDIS_CONNECT_MAX_ATTEMPTS: Connection attempts before startup fails (optional, default: 3)
|
|
9
12
|
*
|
|
10
13
|
* @example
|
|
11
14
|
* ```ts
|
|
@@ -34,9 +37,19 @@ import {
|
|
|
34
37
|
createProvider,
|
|
35
38
|
createProviderInstrumentation,
|
|
36
39
|
} from "@beignet/core/providers";
|
|
37
|
-
import Redis from "ioredis";
|
|
40
|
+
import { Redis } from "ioredis";
|
|
38
41
|
import { z } from "zod";
|
|
39
42
|
|
|
43
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
|
|
44
|
+
const DEFAULT_MAX_RETRIES_PER_REQUEST = 2;
|
|
45
|
+
const DEFAULT_CONNECT_MAX_ATTEMPTS = 3;
|
|
46
|
+
const RECONNECT_BACKOFF_CAP_MS = 2000;
|
|
47
|
+
|
|
48
|
+
const NumberString = z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(/^\d+$/, "Expected a non-negative integer string")
|
|
51
|
+
.transform((value) => Number.parseInt(value, 10));
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Configuration schema for the Redis provider.
|
|
42
55
|
* Validates environment variables with REDIS_ prefix.
|
|
@@ -52,7 +65,27 @@ const RedisConfigSchema = z.object({
|
|
|
52
65
|
* Redis database number (optional).
|
|
53
66
|
* Defaults to 0 if not specified.
|
|
54
67
|
*/
|
|
55
|
-
DB:
|
|
68
|
+
DB: NumberString.optional(),
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Timeout for the initial connection attempt, in milliseconds.
|
|
72
|
+
* Defaults to 5000.
|
|
73
|
+
*/
|
|
74
|
+
CONNECT_TIMEOUT_MS: NumberString.default(DEFAULT_CONNECT_TIMEOUT_MS),
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How many times a single command is retried before its promise rejects.
|
|
78
|
+
* Defaults to 2.
|
|
79
|
+
*/
|
|
80
|
+
MAX_RETRIES_PER_REQUEST: NumberString.default(
|
|
81
|
+
DEFAULT_MAX_RETRIES_PER_REQUEST,
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Connection attempts allowed before startup `connect()` rejects.
|
|
86
|
+
* Defaults to 3.
|
|
87
|
+
*/
|
|
88
|
+
CONNECT_MAX_ATTEMPTS: NumberString.default(DEFAULT_CONNECT_MAX_ATTEMPTS),
|
|
56
89
|
});
|
|
57
90
|
|
|
58
91
|
/**
|
|
@@ -61,278 +94,396 @@ const RedisConfigSchema = z.object({
|
|
|
61
94
|
export type RedisConfig = z.infer<typeof RedisConfigSchema>;
|
|
62
95
|
|
|
63
96
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
97
|
+
* Options for {@link createRedisProvider}.
|
|
98
|
+
*
|
|
99
|
+
* Numeric options become schema defaults, so matching `REDIS_*` environment
|
|
100
|
+
* variables still win when both are set.
|
|
66
101
|
*/
|
|
67
|
-
export interface
|
|
102
|
+
export interface RedisProviderOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Timeout for the initial connection attempt, in milliseconds.
|
|
105
|
+
*
|
|
106
|
+
* @default 5000
|
|
107
|
+
*/
|
|
108
|
+
connectTimeoutMs?: number;
|
|
109
|
+
|
|
68
110
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
111
|
+
* How many times a single command is retried before its promise rejects.
|
|
112
|
+
*
|
|
113
|
+
* @default 2
|
|
114
|
+
*/
|
|
115
|
+
maxRetriesPerRequest?: number;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Custom ioredis retry strategy. Replaces the default strategy, which stops
|
|
119
|
+
* retrying after `REDIS_CONNECT_MAX_ATTEMPTS` during the initial connect and
|
|
120
|
+
* reconnects with capped exponential backoff afterwards.
|
|
121
|
+
*/
|
|
122
|
+
retryStrategy?: (times: number) => number | null;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Redis database number.
|
|
126
|
+
*
|
|
127
|
+
* @default 0
|
|
128
|
+
*/
|
|
129
|
+
db?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Escape hatch for apps that need the raw ioredis client.
|
|
134
|
+
*/
|
|
135
|
+
export interface RedisEscapeHatch {
|
|
136
|
+
/**
|
|
137
|
+
* Raw ioredis client.
|
|
138
|
+
* Use this for Redis operations the stable cache port does not model.
|
|
71
139
|
*/
|
|
72
140
|
client: Redis;
|
|
73
141
|
}
|
|
74
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Ports contributed by the Redis provider.
|
|
145
|
+
*/
|
|
146
|
+
export interface RedisProviderPorts {
|
|
147
|
+
/**
|
|
148
|
+
* Beignet cache port backed by Redis.
|
|
149
|
+
*/
|
|
150
|
+
cache: CachePort;
|
|
151
|
+
/**
|
|
152
|
+
* Raw ioredis client escape hatch.
|
|
153
|
+
*/
|
|
154
|
+
redis: RedisEscapeHatch;
|
|
155
|
+
}
|
|
156
|
+
|
|
75
157
|
function errorMessage(error: unknown): string {
|
|
76
158
|
return error instanceof Error ? error.message : String(error);
|
|
77
159
|
}
|
|
78
160
|
|
|
161
|
+
function reconnectBackoff(attempt: number): number {
|
|
162
|
+
return Math.min(2 ** attempt * 100, RECONNECT_BACKOFF_CAP_MS);
|
|
163
|
+
}
|
|
164
|
+
|
|
79
165
|
/**
|
|
80
|
-
*
|
|
166
|
+
* Create an env-backed Redis cache provider.
|
|
81
167
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* - REDIS_DB: Redis database number (optional)
|
|
168
|
+
* Reads `REDIS_*` env vars, contributes `ports.cache`, and exposes
|
|
169
|
+
* `ports.redis` for raw ioredis client access.
|
|
85
170
|
*
|
|
86
171
|
* @example
|
|
87
172
|
* ```ts
|
|
88
|
-
* const
|
|
89
|
-
* ports: basePorts,
|
|
90
|
-
* providers: [redisProvider],
|
|
91
|
-
* // ...
|
|
92
|
-
* });
|
|
173
|
+
* const provider = createRedisProvider({ connectTimeoutMs: 2000 });
|
|
93
174
|
* ```
|
|
94
175
|
*/
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}) => {
|
|
141
|
-
instrumentation.custom({
|
|
142
|
-
name: `cache.${event.operation}`,
|
|
143
|
-
label: `Cache ${event.operation}`,
|
|
144
|
-
summary: event.summary,
|
|
145
|
-
details: {
|
|
146
|
-
operation: event.operation,
|
|
147
|
-
key: event.key,
|
|
148
|
-
...event.details,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Extend ports with cache API
|
|
154
|
-
const cache: RedisCachePort = {
|
|
155
|
-
get: async (key: string) => {
|
|
156
|
-
const startedAt = Date.now();
|
|
157
|
-
try {
|
|
158
|
-
const value = await client.get(key);
|
|
159
|
-
recordCacheEvent({
|
|
160
|
-
operation: "get",
|
|
161
|
-
key,
|
|
162
|
-
summary: value == null ? "Cache miss" : "Cache hit",
|
|
163
|
-
details: {
|
|
164
|
-
hit: value != null,
|
|
165
|
-
durationMs: Date.now() - startedAt,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
return value;
|
|
169
|
-
} catch (error) {
|
|
170
|
-
recordCacheEvent({
|
|
171
|
-
operation: "get.failed",
|
|
172
|
-
key,
|
|
173
|
-
summary: "Cache get failed",
|
|
174
|
-
details: {
|
|
175
|
-
durationMs: Date.now() - startedAt,
|
|
176
|
-
error: errorMessage(error),
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
set: async (key, value, options) => {
|
|
184
|
-
const startedAt = Date.now();
|
|
185
|
-
try {
|
|
186
|
-
if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
|
|
187
|
-
await client.set(key, value, "EX", options.ttlSeconds);
|
|
188
|
-
} else {
|
|
189
|
-
await client.set(key, value);
|
|
176
|
+
export function createRedisProvider(options: RedisProviderOptions = {}) {
|
|
177
|
+
const ConfigSchema = RedisConfigSchema.extend({
|
|
178
|
+
DB:
|
|
179
|
+
options.db !== undefined
|
|
180
|
+
? NumberString.default(options.db)
|
|
181
|
+
: RedisConfigSchema.shape.DB,
|
|
182
|
+
CONNECT_TIMEOUT_MS:
|
|
183
|
+
options.connectTimeoutMs !== undefined
|
|
184
|
+
? NumberString.default(options.connectTimeoutMs)
|
|
185
|
+
: RedisConfigSchema.shape.CONNECT_TIMEOUT_MS,
|
|
186
|
+
MAX_RETRIES_PER_REQUEST:
|
|
187
|
+
options.maxRetriesPerRequest !== undefined
|
|
188
|
+
? NumberString.default(options.maxRetriesPerRequest)
|
|
189
|
+
: RedisConfigSchema.shape.MAX_RETRIES_PER_REQUEST,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return createProvider({
|
|
193
|
+
name: "redis",
|
|
194
|
+
|
|
195
|
+
config: {
|
|
196
|
+
schema: ConfigSchema,
|
|
197
|
+
envPrefix: "REDIS_",
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
async setup({ ports, config }) {
|
|
201
|
+
if (!config) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"[redisProvider] Missing Redis config. " +
|
|
204
|
+
"Please set REDIS_URL environment variable.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const connectMaxAttempts =
|
|
209
|
+
config.CONNECT_MAX_ATTEMPTS ?? DEFAULT_CONNECT_MAX_ATTEMPTS;
|
|
210
|
+
|
|
211
|
+
// ioredis uses a single retryStrategy for both the initial connect and
|
|
212
|
+
// later reconnects, so track whether the first connect ever succeeded.
|
|
213
|
+
let connectedOnce = false;
|
|
214
|
+
|
|
215
|
+
const defaultRetryStrategy = (attempt: number): number | null => {
|
|
216
|
+
if (!connectedOnce) {
|
|
217
|
+
// Stop retrying during the initial connect so `connect()` below
|
|
218
|
+
// rejects promptly instead of hanging against an unreachable host.
|
|
219
|
+
if (attempt >= connectMaxAttempts) {
|
|
220
|
+
return null;
|
|
190
221
|
}
|
|
191
|
-
|
|
192
|
-
operation: "set",
|
|
193
|
-
key,
|
|
194
|
-
summary: "Cache set",
|
|
195
|
-
details: {
|
|
196
|
-
ttlSeconds: options?.ttlSeconds ?? null,
|
|
197
|
-
durationMs: Date.now() - startedAt,
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
} catch (error) {
|
|
201
|
-
recordCacheEvent({
|
|
202
|
-
operation: "set.failed",
|
|
203
|
-
key,
|
|
204
|
-
summary: "Cache set failed",
|
|
205
|
-
details: {
|
|
206
|
-
ttlSeconds: options?.ttlSeconds ?? null,
|
|
207
|
-
durationMs: Date.now() - startedAt,
|
|
208
|
-
error: errorMessage(error),
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
throw error;
|
|
222
|
+
return reconnectBackoff(attempt);
|
|
212
223
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
224
|
+
// After a successful connection, keep reconnecting with capped
|
|
225
|
+
// exponential backoff.
|
|
226
|
+
return reconnectBackoff(attempt);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const client = new Redis(config.URL, {
|
|
230
|
+
db: config.DB ?? options.db ?? 0,
|
|
231
|
+
// Defer connecting until the explicit connect() below so startup can
|
|
232
|
+
// surface a clear error instead of connecting in the background.
|
|
233
|
+
lazyConnect: true,
|
|
234
|
+
connectTimeout:
|
|
235
|
+
config.CONNECT_TIMEOUT_MS ??
|
|
236
|
+
options.connectTimeoutMs ??
|
|
237
|
+
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
238
|
+
maxRetriesPerRequest:
|
|
239
|
+
config.MAX_RETRIES_PER_REQUEST ??
|
|
240
|
+
options.maxRetriesPerRequest ??
|
|
241
|
+
DEFAULT_MAX_RETRIES_PER_REQUEST,
|
|
242
|
+
retryStrategy: options.retryStrategy ?? defaultRetryStrategy,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Test connection
|
|
246
|
+
try {
|
|
247
|
+
await client.connect();
|
|
248
|
+
connectedOnce = true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
client.disconnect();
|
|
251
|
+
throw new Error(
|
|
252
|
+
`[redisProvider] Failed to connect to Redis at ${config.URL}: ${
|
|
253
|
+
error instanceof Error ? error.message : String(error)
|
|
254
|
+
}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
259
|
+
providerName: "redis",
|
|
260
|
+
watcher: "cache",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const recordCacheEvent = (event: {
|
|
264
|
+
operation: string;
|
|
265
|
+
key: string;
|
|
266
|
+
summary: string;
|
|
267
|
+
details?: Record<string, unknown>;
|
|
268
|
+
}) => {
|
|
269
|
+
instrumentation.custom({
|
|
270
|
+
name: `cache.${event.operation}`,
|
|
271
|
+
label: `Cache ${event.operation}`,
|
|
272
|
+
summary: event.summary,
|
|
273
|
+
details: {
|
|
274
|
+
operation: event.operation,
|
|
275
|
+
key: event.key,
|
|
276
|
+
...event.details,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Extend ports with cache API
|
|
282
|
+
const cache: CachePort = {
|
|
283
|
+
get: async (key: string) => {
|
|
284
|
+
const startedAt = Date.now();
|
|
285
|
+
try {
|
|
286
|
+
const value = await client.get(key);
|
|
287
|
+
recordCacheEvent({
|
|
288
|
+
operation: "get",
|
|
289
|
+
key,
|
|
290
|
+
summary: value == null ? "Cache miss" : "Cache hit",
|
|
291
|
+
details: {
|
|
292
|
+
hit: value != null,
|
|
293
|
+
durationMs: Date.now() - startedAt,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
return value;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
recordCacheEvent({
|
|
299
|
+
operation: "get.failed",
|
|
300
|
+
key,
|
|
301
|
+
summary: "Cache get failed",
|
|
302
|
+
details: {
|
|
303
|
+
durationMs: Date.now() - startedAt,
|
|
304
|
+
error: errorMessage(error),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
set: async (key, value, options) => {
|
|
312
|
+
const startedAt = Date.now();
|
|
313
|
+
try {
|
|
314
|
+
if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
|
|
315
|
+
await client.set(key, value, "EX", options.ttlSeconds);
|
|
316
|
+
} else {
|
|
317
|
+
await client.set(key, value);
|
|
318
|
+
}
|
|
319
|
+
recordCacheEvent({
|
|
320
|
+
operation: "set",
|
|
321
|
+
key,
|
|
322
|
+
summary: "Cache set",
|
|
323
|
+
details: {
|
|
324
|
+
ttlSeconds: options?.ttlSeconds ?? null,
|
|
325
|
+
durationMs: Date.now() - startedAt,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
} catch (error) {
|
|
329
|
+
recordCacheEvent({
|
|
330
|
+
operation: "set.failed",
|
|
331
|
+
key,
|
|
332
|
+
summary: "Cache set failed",
|
|
333
|
+
details: {
|
|
334
|
+
ttlSeconds: options?.ttlSeconds ?? null,
|
|
335
|
+
durationMs: Date.now() - startedAt,
|
|
336
|
+
error: errorMessage(error),
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
delete: async (key) => {
|
|
344
|
+
const startedAt = Date.now();
|
|
345
|
+
try {
|
|
346
|
+
const deleted = (await client.del(key)) > 0;
|
|
347
|
+
recordCacheEvent({
|
|
348
|
+
operation: "delete",
|
|
349
|
+
key,
|
|
350
|
+
summary: deleted ? "Cache key deleted" : "Cache key missing",
|
|
351
|
+
details: {
|
|
352
|
+
deleted,
|
|
353
|
+
durationMs: Date.now() - startedAt,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
return deleted;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
recordCacheEvent({
|
|
359
|
+
operation: "delete.failed",
|
|
360
|
+
key,
|
|
361
|
+
summary: "Cache delete failed",
|
|
362
|
+
details: {
|
|
363
|
+
durationMs: Date.now() - startedAt,
|
|
364
|
+
error: errorMessage(error),
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
has: async (key) => {
|
|
372
|
+
const startedAt = Date.now();
|
|
373
|
+
try {
|
|
374
|
+
const result = await client.exists(key);
|
|
375
|
+
const exists = result === 1;
|
|
376
|
+
recordCacheEvent({
|
|
377
|
+
operation: "has",
|
|
378
|
+
key,
|
|
379
|
+
summary: exists ? "Cache key exists" : "Cache key missing",
|
|
380
|
+
details: {
|
|
381
|
+
exists,
|
|
382
|
+
durationMs: Date.now() - startedAt,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
return exists;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
recordCacheEvent({
|
|
388
|
+
operation: "has.failed",
|
|
389
|
+
key,
|
|
390
|
+
summary: "Cache has failed",
|
|
391
|
+
details: {
|
|
392
|
+
durationMs: Date.now() - startedAt,
|
|
393
|
+
error: errorMessage(error),
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
remember: async (key, factory, options) => {
|
|
401
|
+
const startedAt = Date.now();
|
|
402
|
+
try {
|
|
403
|
+
const cached = await client.get(key);
|
|
404
|
+
if (cached != null) {
|
|
405
|
+
recordCacheEvent({
|
|
406
|
+
operation: "remember",
|
|
407
|
+
key,
|
|
408
|
+
summary: "Cache remember hit",
|
|
409
|
+
details: {
|
|
410
|
+
hit: true,
|
|
411
|
+
ttlSeconds: options?.ttlSeconds ?? null,
|
|
412
|
+
durationMs: Date.now() - startedAt,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
return cached;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const value = await factory();
|
|
419
|
+
if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
|
|
420
|
+
await client.set(key, value, "EX", options.ttlSeconds);
|
|
421
|
+
} else {
|
|
422
|
+
await client.set(key, value);
|
|
423
|
+
}
|
|
271
424
|
|
|
272
|
-
remember: async (key, factory, options) => {
|
|
273
|
-
const startedAt = Date.now();
|
|
274
|
-
try {
|
|
275
|
-
const cached = await client.get(key);
|
|
276
|
-
if (cached != null) {
|
|
277
425
|
recordCacheEvent({
|
|
278
426
|
operation: "remember",
|
|
279
427
|
key,
|
|
280
|
-
summary: "Cache remember
|
|
428
|
+
summary: "Cache remember miss",
|
|
281
429
|
details: {
|
|
282
|
-
hit:
|
|
430
|
+
hit: false,
|
|
283
431
|
ttlSeconds: options?.ttlSeconds ?? null,
|
|
284
432
|
durationMs: Date.now() - startedAt,
|
|
285
433
|
},
|
|
286
434
|
});
|
|
287
|
-
|
|
435
|
+
|
|
436
|
+
return value;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
recordCacheEvent({
|
|
439
|
+
operation: "remember.failed",
|
|
440
|
+
key,
|
|
441
|
+
summary: "Cache remember failed",
|
|
442
|
+
details: {
|
|
443
|
+
ttlSeconds: options?.ttlSeconds ?? null,
|
|
444
|
+
durationMs: Date.now() - startedAt,
|
|
445
|
+
error: errorMessage(error),
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
throw error;
|
|
288
449
|
}
|
|
450
|
+
},
|
|
451
|
+
};
|
|
289
452
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
453
|
+
return {
|
|
454
|
+
ports: {
|
|
455
|
+
cache,
|
|
456
|
+
redis: { client },
|
|
457
|
+
} satisfies RedisProviderPorts,
|
|
458
|
+
async stop() {
|
|
459
|
+
try {
|
|
460
|
+
await client.quit();
|
|
461
|
+
} catch {
|
|
462
|
+
// Silently ignore errors during shutdown
|
|
295
463
|
}
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
296
469
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
error: errorMessage(error),
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
throw error;
|
|
321
|
-
}
|
|
322
|
-
},
|
|
323
|
-
|
|
324
|
-
client,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
return {
|
|
328
|
-
ports: { cache },
|
|
329
|
-
async stop() {
|
|
330
|
-
try {
|
|
331
|
-
await client.quit();
|
|
332
|
-
} catch {
|
|
333
|
-
// Silently ignore errors during shutdown
|
|
334
|
-
}
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
},
|
|
338
|
-
});
|
|
470
|
+
/**
|
|
471
|
+
* Default env-backed Redis provider.
|
|
472
|
+
*
|
|
473
|
+
* Configuration via environment variables:
|
|
474
|
+
* - REDIS_URL: Redis connection URL (required)
|
|
475
|
+
* - REDIS_DB: Redis database number (optional)
|
|
476
|
+
* - REDIS_CONNECT_TIMEOUT_MS: Initial connection timeout in ms (optional)
|
|
477
|
+
* - REDIS_MAX_RETRIES_PER_REQUEST: Per-command retry limit (optional)
|
|
478
|
+
* - REDIS_CONNECT_MAX_ATTEMPTS: Connection attempts before startup fails (optional)
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const server = await createNextServer({
|
|
483
|
+
* ports: basePorts,
|
|
484
|
+
* providers: [redisProvider],
|
|
485
|
+
* // ...
|
|
486
|
+
* });
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
export const redisProvider = createRedisProvider();
|