@uploadista/kv-store-redis 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +575 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/redis-kv-store.d.ts +9 -0
- package/dist/redis-kv-store.d.ts.map +1 -0
- package/dist/redis-kv-store.js +40 -0
- package/package.json +29 -0
- package/src/index.ts +1 -0
- package/src/redis-kv-store.ts +65 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
# @uploadista/kv-store-redis
|
|
2
|
+
|
|
3
|
+
Redis-backed key-value store for Uploadista. Provides distributed, persistent storage with high performance and clustering support.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Redis KV store uses the official `@redis/client` library to connect to Redis servers. It's designed for:
|
|
8
|
+
|
|
9
|
+
- **Distributed Systems**: Share state across multiple server instances
|
|
10
|
+
- **Persistent Storage**: Data survives process restarts
|
|
11
|
+
- **Production Deployments**: High availability and clustering
|
|
12
|
+
- **Real-Time Applications**: Pub/Sub support for event broadcasting
|
|
13
|
+
- **Scaling**: Horizontal scaling with Redis clusters
|
|
14
|
+
|
|
15
|
+
Supports Redis 5.0+ running standalone, in master-replica setup, or clustered mode.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @uploadista/kv-store-redis @redis/client
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @uploadista/kv-store-redis @redis/client
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
- Node.js 18+
|
|
28
|
+
- Redis 5.0+ server running and accessible
|
|
29
|
+
- Network connectivity to Redis instance
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
35
|
+
import { createClient } from "@redis/client";
|
|
36
|
+
import { Effect } from "effect";
|
|
37
|
+
|
|
38
|
+
// Create Redis client
|
|
39
|
+
const redisClient = createClient({
|
|
40
|
+
url: "redis://localhost:6379",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await redisClient.connect();
|
|
44
|
+
|
|
45
|
+
// Use the KV store layer
|
|
46
|
+
const program = Effect.gen(function* () {
|
|
47
|
+
// The redisKvStore is automatically available
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
Effect.runSync(
|
|
51
|
+
program.pipe(
|
|
52
|
+
Effect.provide(redisKvStore({ redis: redisClient }))
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- ✅ **Distributed State**: Share uploads and flows across servers
|
|
60
|
+
- ✅ **Persistence**: Data persists between deployments (with RDB/AOF)
|
|
61
|
+
- ✅ **High Performance**: Single-digit millisecond latency
|
|
62
|
+
- ✅ **Clustering**: Support for Redis clusters with replicas
|
|
63
|
+
- ✅ **Pub/Sub Ready**: Integration with event broadcasting
|
|
64
|
+
- ✅ **Automatic Failover**: Works with sentinel-monitored Redis
|
|
65
|
+
- ✅ **Type Safe**: Full TypeScript support
|
|
66
|
+
|
|
67
|
+
## API Reference
|
|
68
|
+
|
|
69
|
+
### Main Exports
|
|
70
|
+
|
|
71
|
+
#### `redisKvStore(config: RedisKvStoreConfig): Layer<BaseKvStoreService>`
|
|
72
|
+
|
|
73
|
+
Creates an Effect layer providing the `BaseKvStoreService` backed by Redis.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
77
|
+
import { createClient } from "@redis/client";
|
|
78
|
+
|
|
79
|
+
const redisClient = createClient({ url: "redis://localhost:6379" });
|
|
80
|
+
await redisClient.connect();
|
|
81
|
+
|
|
82
|
+
const layer = redisKvStore({ redis: redisClient });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Configuration**:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface RedisKvStoreConfig {
|
|
89
|
+
redis: RedisClientType; // Connected @redis/client instance
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `makeRedisBaseKvStore(config: RedisKvStoreConfig): BaseKvStore`
|
|
94
|
+
|
|
95
|
+
Factory function for creating a KV store with an existing Redis client.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { makeRedisBaseKvStore } from "@uploadista/kv-store-redis";
|
|
99
|
+
import { createClient } from "@redis/client";
|
|
100
|
+
|
|
101
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
102
|
+
await redis.connect();
|
|
103
|
+
|
|
104
|
+
const store = makeRedisBaseKvStore({ redis });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Available Operations
|
|
108
|
+
|
|
109
|
+
The Redis store implements the `BaseKvStore` interface with automatic serialization:
|
|
110
|
+
|
|
111
|
+
#### `get(key: string): Effect<string | null>`
|
|
112
|
+
|
|
113
|
+
Retrieve a value by key. Returns `null` if key doesn't exist.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const program = Effect.gen(function* () {
|
|
117
|
+
const value = yield* store.get("user:123");
|
|
118
|
+
// Fetches from Redis with automatic error handling
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `set(key: string, value: string): Effect<void>`
|
|
123
|
+
|
|
124
|
+
Store a string value. Overwrites existing value if key exists.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const program = Effect.gen(function* () {
|
|
128
|
+
yield* store.set("user:123", JSON.stringify({ name: "Alice" }));
|
|
129
|
+
// Persisted in Redis
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `delete(key: string): Effect<void>`
|
|
134
|
+
|
|
135
|
+
Remove a key from Redis. Safe to call on non-existent keys.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const program = Effect.gen(function* () {
|
|
139
|
+
yield* store.delete("user:123");
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `list(keyPrefix: string): Effect<string[]>`
|
|
144
|
+
|
|
145
|
+
List keys matching a prefix using Redis SCAN (cursor-based, non-blocking).
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const program = Effect.gen(function* () {
|
|
149
|
+
const keys = yield* store.list("user:");
|
|
150
|
+
// Returns: ["123", "456", "789"]
|
|
151
|
+
// Uses SCAN internally to avoid blocking Redis
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
### Basic Setup
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
161
|
+
import { createClient } from "@redis/client";
|
|
162
|
+
|
|
163
|
+
const redis = createClient({
|
|
164
|
+
url: "redis://localhost:6379",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await redis.connect();
|
|
168
|
+
|
|
169
|
+
const layer = redisKvStore({ redis });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Environment-Based Configuration
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
176
|
+
import { createClient } from "@redis/client";
|
|
177
|
+
|
|
178
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
179
|
+
|
|
180
|
+
const redis = createClient({
|
|
181
|
+
url: redisUrl,
|
|
182
|
+
password: process.env.REDIS_PASSWORD,
|
|
183
|
+
db: parseInt(process.env.REDIS_DB || "0"),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await redis.connect();
|
|
187
|
+
|
|
188
|
+
const layer = redisKvStore({ redis });
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Master-Replica Setup
|
|
192
|
+
|
|
193
|
+
For high availability with automatic failover:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
197
|
+
import { createClient } from "@redis/client";
|
|
198
|
+
|
|
199
|
+
const redis = createClient({
|
|
200
|
+
socket: {
|
|
201
|
+
host: process.env.REDIS_HOST,
|
|
202
|
+
port: parseInt(process.env.REDIS_PORT || "6379"),
|
|
203
|
+
},
|
|
204
|
+
password: process.env.REDIS_PASSWORD,
|
|
205
|
+
// Reads can use replicas
|
|
206
|
+
readonly: true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await redis.connect();
|
|
210
|
+
|
|
211
|
+
const layer = redisKvStore({ redis });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Redis Cluster Setup
|
|
215
|
+
|
|
216
|
+
For horizontal scaling:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
220
|
+
import { createCluster } from "@redis/client";
|
|
221
|
+
|
|
222
|
+
const cluster = createCluster({
|
|
223
|
+
rootNodes: [
|
|
224
|
+
{ host: "node1", port: 6379 },
|
|
225
|
+
{ host: "node2", port: 6379 },
|
|
226
|
+
{ host: "node3", port: 6379 },
|
|
227
|
+
],
|
|
228
|
+
defaults: {
|
|
229
|
+
password: process.env.REDIS_PASSWORD,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await cluster.connect();
|
|
234
|
+
|
|
235
|
+
const layer = redisKvStore({ redis: cluster as any }); // Cast needed for cluster
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Examples
|
|
239
|
+
|
|
240
|
+
### Example 1: Distributed Upload Server
|
|
241
|
+
|
|
242
|
+
Multiple servers sharing upload state via Redis:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
246
|
+
import { uploadServer } from "@uploadista/server";
|
|
247
|
+
import { createClient } from "@redis/client";
|
|
248
|
+
import { Effect } from "effect";
|
|
249
|
+
|
|
250
|
+
const redis = createClient({ url: "redis://redis-server:6379" });
|
|
251
|
+
await redis.connect();
|
|
252
|
+
|
|
253
|
+
const program = Effect.gen(function* () {
|
|
254
|
+
const server = yield* uploadServer;
|
|
255
|
+
|
|
256
|
+
// Server can handle requests on any instance
|
|
257
|
+
// Upload state is stored in shared Redis
|
|
258
|
+
const upload = yield* server.createUpload(
|
|
259
|
+
{ filename: "file.pdf", size: 1024 },
|
|
260
|
+
"client-1"
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
console.log(upload.id); // Accessible from any server instance
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
Effect.runSync(
|
|
267
|
+
program.pipe(
|
|
268
|
+
Effect.provide(redisKvStore({ redis })),
|
|
269
|
+
// ... other layers
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Example 2: Rate Limiting
|
|
275
|
+
|
|
276
|
+
Use Redis to track and limit upload attempts:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { makeRedisBaseKvStore } from "@uploadista/kv-store-redis";
|
|
280
|
+
import { createClient } from "@redis/client";
|
|
281
|
+
import { Effect } from "effect";
|
|
282
|
+
|
|
283
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
284
|
+
await redis.connect();
|
|
285
|
+
|
|
286
|
+
const store = makeRedisBaseKvStore({ redis });
|
|
287
|
+
|
|
288
|
+
const checkRateLimit = (clientId: string, limit: number = 10) =>
|
|
289
|
+
Effect.gen(function* () {
|
|
290
|
+
const key = `ratelimit:${clientId}`;
|
|
291
|
+
const count = yield* store.get(key);
|
|
292
|
+
|
|
293
|
+
const current = count ? parseInt(count) : 0;
|
|
294
|
+
|
|
295
|
+
if (current >= limit) {
|
|
296
|
+
return false; // Rate limited
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Increment counter
|
|
300
|
+
yield* store.set(key, String(current + 1));
|
|
301
|
+
|
|
302
|
+
return true; // Allowed
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Usage
|
|
306
|
+
const program = Effect.gen(function* () {
|
|
307
|
+
const allowed = yield* checkRateLimit("user:123", 100);
|
|
308
|
+
console.log(allowed ? "Upload allowed" : "Rate limited");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
Effect.runSync(program);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Example 3: Session Management
|
|
315
|
+
|
|
316
|
+
Store user session data:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { makeRedisBaseKvStore } from "@uploadista/kv-store-redis";
|
|
320
|
+
import { createClient } from "@redis/client";
|
|
321
|
+
import { Effect } from "effect";
|
|
322
|
+
|
|
323
|
+
const redis = createClient({ url: "redis://localhost:6379" });
|
|
324
|
+
await redis.connect();
|
|
325
|
+
|
|
326
|
+
const store = makeRedisBaseKvStore({ redis });
|
|
327
|
+
|
|
328
|
+
interface Session {
|
|
329
|
+
userId: string;
|
|
330
|
+
loginTime: number;
|
|
331
|
+
lastActivity: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const storeSession = (sessionId: string, session: Session) =>
|
|
335
|
+
Effect.gen(function* () {
|
|
336
|
+
yield* store.set(`session:${sessionId}`, JSON.stringify(session));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const getSession = (sessionId: string) =>
|
|
340
|
+
Effect.gen(function* () {
|
|
341
|
+
const data = yield* store.get(`session:${sessionId}`);
|
|
342
|
+
return data ? JSON.parse(data) : null;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Usage
|
|
346
|
+
const program = Effect.gen(function* () {
|
|
347
|
+
const session: Session = {
|
|
348
|
+
userId: "user:123",
|
|
349
|
+
loginTime: Date.now(),
|
|
350
|
+
lastActivity: Date.now(),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
yield* storeSession("sess_abc123", session);
|
|
354
|
+
|
|
355
|
+
const retrieved = yield* getSession("sess_abc123");
|
|
356
|
+
console.log(retrieved);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
Effect.runSync(program);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Performance Tuning
|
|
363
|
+
|
|
364
|
+
### Connection Pooling
|
|
365
|
+
|
|
366
|
+
For high-throughput applications, connection pooling is automatic but tune if needed:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { createClient } from "@redis/client";
|
|
370
|
+
|
|
371
|
+
const redis = createClient({
|
|
372
|
+
url: "redis://localhost:6379",
|
|
373
|
+
socket: {
|
|
374
|
+
keepAlive: 30000, // Keep connections alive
|
|
375
|
+
noDelay: true, // Disable Nagle's algorithm for low latency
|
|
376
|
+
},
|
|
377
|
+
// Pipelining is automatic - commands are batched
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await redis.connect();
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Key Naming Strategy
|
|
384
|
+
|
|
385
|
+
For efficient SCAN operations:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Good: Hierarchical naming with colons
|
|
389
|
+
"upload:abc123"
|
|
390
|
+
"upload:abc123:chunk:0"
|
|
391
|
+
"session:user123"
|
|
392
|
+
"ratelimit:client456"
|
|
393
|
+
|
|
394
|
+
// Avoid: Long keys or random prefixes
|
|
395
|
+
"up_abc123_data"
|
|
396
|
+
"sess_user123_session_data"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Memory Management
|
|
400
|
+
|
|
401
|
+
Monitor and manage Redis memory:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const info = await redis.info("memory");
|
|
405
|
+
console.log(info);
|
|
406
|
+
// "memory:peak_allocated": 1048576
|
|
407
|
+
// "memory:used": 524288
|
|
408
|
+
|
|
409
|
+
// Set eviction policy in redis.conf:
|
|
410
|
+
// maxmemory 2gb
|
|
411
|
+
// maxmemory-policy allkeys-lru
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Scaling Patterns
|
|
415
|
+
|
|
416
|
+
### Single Instance
|
|
417
|
+
|
|
418
|
+
For development and small deployments:
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
Client ──→ Redis (single instance)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Master-Replica
|
|
425
|
+
|
|
426
|
+
For read-heavy workloads:
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
Read-Only Clients ──→ Replica 1 ─┐
|
|
430
|
+
├─ Master (writes) ──→ Replicas
|
|
431
|
+
Read-Only Clients ──→ Replica 2 ─┘
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Cluster
|
|
435
|
+
|
|
436
|
+
For massive scale (100GB+ data):
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
Clients ──→ Redis Cluster (16 shards)
|
|
440
|
+
- Automatic data distribution
|
|
441
|
+
- Automatic failover
|
|
442
|
+
- Linear scaling
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Deployment Options
|
|
446
|
+
|
|
447
|
+
### Docker Compose
|
|
448
|
+
|
|
449
|
+
```yaml
|
|
450
|
+
version: "3"
|
|
451
|
+
services:
|
|
452
|
+
app:
|
|
453
|
+
build: .
|
|
454
|
+
environment:
|
|
455
|
+
REDIS_URL: redis://redis:6379
|
|
456
|
+
depends_on:
|
|
457
|
+
- redis
|
|
458
|
+
redis:
|
|
459
|
+
image: redis:7-alpine
|
|
460
|
+
ports:
|
|
461
|
+
- "6379:6379"
|
|
462
|
+
volumes:
|
|
463
|
+
- redis_data:/data
|
|
464
|
+
command: redis-server --appendonly yes
|
|
465
|
+
|
|
466
|
+
volumes:
|
|
467
|
+
redis_data:
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Kubernetes
|
|
471
|
+
|
|
472
|
+
```yaml
|
|
473
|
+
apiVersion: apps/v1
|
|
474
|
+
kind: Deployment
|
|
475
|
+
metadata:
|
|
476
|
+
name: uploadista-app
|
|
477
|
+
spec:
|
|
478
|
+
replicas: 3
|
|
479
|
+
template:
|
|
480
|
+
spec:
|
|
481
|
+
containers:
|
|
482
|
+
- name: app
|
|
483
|
+
env:
|
|
484
|
+
- name: REDIS_URL
|
|
485
|
+
value: redis://redis-cluster.default.svc.cluster.local:6379
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Managed Services
|
|
489
|
+
|
|
490
|
+
- **AWS ElastiCache**: `rediscloud.c6g.large` on AWS
|
|
491
|
+
- **Heroku Redis**: `heroku-redis:premium-2`
|
|
492
|
+
- **UpCloud Managed Database**: Production-grade Redis
|
|
493
|
+
|
|
494
|
+
## Related Packages
|
|
495
|
+
|
|
496
|
+
- [@uploadista/core](../../core) - Core types and interfaces
|
|
497
|
+
- [@uploadista/kv-store-ioredis](../ioredis) - Alternative Redis client with clustering
|
|
498
|
+
- [@uploadista/kv-store-memory](../memory) - For development/testing
|
|
499
|
+
- [@uploadista/kv-store-cloudflare-kv](../cloudflare-kv) - Edge deployment
|
|
500
|
+
- [@uploadista/event-broadcaster-redis](../../event-emitters/event-broadcaster-redis) - Event broadcasting with Redis
|
|
501
|
+
- [@uploadista/server](../../servers/server) - Upload server using KV stores
|
|
502
|
+
|
|
503
|
+
## Troubleshooting
|
|
504
|
+
|
|
505
|
+
### Connection Refused
|
|
506
|
+
|
|
507
|
+
```
|
|
508
|
+
Error: connect ECONNREFUSED 127.0.0.1:6379
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Solutions:
|
|
512
|
+
1. Verify Redis is running: `redis-cli ping`
|
|
513
|
+
2. Check hostname/port configuration
|
|
514
|
+
3. Verify firewall rules if remote Redis
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
# Test connection
|
|
518
|
+
redis-cli -h redis-host -p 6379 ping
|
|
519
|
+
# Should return: PONG
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Memory Issues
|
|
523
|
+
|
|
524
|
+
If Redis memory grows unexpectedly:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# Check which keys consume most memory
|
|
528
|
+
redis-cli --bigkeys
|
|
529
|
+
|
|
530
|
+
# Set max memory policy
|
|
531
|
+
redis-cli CONFIG SET maxmemory 2gb
|
|
532
|
+
redis-cli CONFIG SET maxmemory-policy allkeys-lru
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Slow SCAN Operations
|
|
536
|
+
|
|
537
|
+
If `list()` operations are slow:
|
|
538
|
+
|
|
539
|
+
1. Reduce key volume or use hierarchical naming
|
|
540
|
+
2. Implement custom scanning with batch limits
|
|
541
|
+
3. Consider Redis cluster for distribution
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// Optimize SCAN by reducing results returned
|
|
545
|
+
yield* store.list("upload:"); // Might scan many keys
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Connection Timeouts
|
|
549
|
+
|
|
550
|
+
For unreliable networks:
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
const redis = createClient({
|
|
554
|
+
url: "redis://redis-host:6379",
|
|
555
|
+
socket: {
|
|
556
|
+
reconnectStrategy: (attempt: number) => {
|
|
557
|
+
if (attempt > 10) return new Error("Max retries exceeded");
|
|
558
|
+
return Math.min(attempt * 50, 500);
|
|
559
|
+
},
|
|
560
|
+
connectTimeout: 5000,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## License
|
|
566
|
+
|
|
567
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
568
|
+
|
|
569
|
+
## See Also
|
|
570
|
+
|
|
571
|
+
- [KV Stores Comparison Guide](../KV_STORES_COMPARISON.md) - Compare Redis with other options
|
|
572
|
+
- [Server Setup Guide](../../../SERVER_SETUP.md) - Redis in production servers
|
|
573
|
+
- [Event Broadcasting with Redis](../../event-emitters/event-broadcaster-redis/README.md) - Pub/Sub patterns
|
|
574
|
+
- [Redis Documentation](https://redis.io/documentation) - Official Redis docs
|
|
575
|
+
- [@redis/client Docs](https://github.com/redis/node-redis) - Node.js Redis client
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./redis-kv-store";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RedisClientType } from "@redis/client";
|
|
2
|
+
import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
export interface RedisKvStoreConfig {
|
|
5
|
+
redis: RedisClientType;
|
|
6
|
+
}
|
|
7
|
+
export declare function makeRedisBaseKvStore({ redis, }: RedisKvStoreConfig): BaseKvStore;
|
|
8
|
+
export declare const redisKvStore: (config: RedisKvStoreConfig) => Layer.Layer<BaseKvStoreService, never, never>;
|
|
9
|
+
//# sourceMappingURL=redis-kv-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-kv-store.d.ts","sourceRoot":"","sources":["../src/redis-kv-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAE,MAAM,eAAe,CAAC;AAGpE,OAAO,EAAE,KAAK,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,eAAe,CAAC;CACxB;AAGD,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,GACN,EAAE,kBAAkB,GAAG,WAAW,CA+ClC;AAGD,eAAO,MAAM,YAAY,GAAI,QAAQ,kBAAkB,kDACU,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { BaseKvStoreService } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
// Base Redis KV store that stores raw strings
|
|
5
|
+
export function makeRedisBaseKvStore({ redis, }) {
|
|
6
|
+
return {
|
|
7
|
+
get: (key) => Effect.tryPromise({
|
|
8
|
+
try: () => redis.get(key),
|
|
9
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
10
|
+
}),
|
|
11
|
+
set: (key, value) => Effect.tryPromise({
|
|
12
|
+
try: () => redis.set(key, value),
|
|
13
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
14
|
+
}).pipe(Effect.asVoid),
|
|
15
|
+
delete: (key) => Effect.tryPromise({
|
|
16
|
+
try: () => redis.del(key),
|
|
17
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
18
|
+
}).pipe(Effect.asVoid),
|
|
19
|
+
list: (keyPrefix) => Effect.gen(function* (_) {
|
|
20
|
+
const keys = new Set();
|
|
21
|
+
let cursor = "0";
|
|
22
|
+
do {
|
|
23
|
+
const result = yield* _(Effect.tryPromise({
|
|
24
|
+
try: () => redis.scan(cursor, {
|
|
25
|
+
MATCH: `${keyPrefix}*`,
|
|
26
|
+
COUNT: 20,
|
|
27
|
+
}),
|
|
28
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
29
|
+
}));
|
|
30
|
+
cursor = result.cursor;
|
|
31
|
+
for (const key of result.keys) {
|
|
32
|
+
keys.add(key.replace(keyPrefix, ""));
|
|
33
|
+
}
|
|
34
|
+
} while (cursor !== "0");
|
|
35
|
+
return Array.from(keys);
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Base store layer
|
|
40
|
+
export const redisKvStore = (config) => Layer.succeed(BaseKvStoreService, makeRedisBaseKvStore(config));
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/kv-store-redis",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "Redis KV store for Uploadista",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Uploadista",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@redis/client": "5.8.3",
|
|
17
|
+
"effect": "3.18.4",
|
|
18
|
+
"@uploadista/core": "0.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b",
|
|
25
|
+
"format": "biome format --write ./src",
|
|
26
|
+
"lint": "biome lint --write ./src",
|
|
27
|
+
"check": "biome check --write ./src"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./redis-kv-store";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RedisArgument, RedisClientType } from "@redis/client";
|
|
2
|
+
|
|
3
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
4
|
+
import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
|
|
7
|
+
export interface RedisKvStoreConfig {
|
|
8
|
+
redis: RedisClientType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Base Redis KV store that stores raw strings
|
|
12
|
+
export function makeRedisBaseKvStore({
|
|
13
|
+
redis,
|
|
14
|
+
}: RedisKvStoreConfig): BaseKvStore {
|
|
15
|
+
return {
|
|
16
|
+
get: (key: string) =>
|
|
17
|
+
Effect.tryPromise({
|
|
18
|
+
try: () => redis.get(key),
|
|
19
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
20
|
+
}),
|
|
21
|
+
|
|
22
|
+
set: (key: string, value: string) =>
|
|
23
|
+
Effect.tryPromise({
|
|
24
|
+
try: () => redis.set(key, value),
|
|
25
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
26
|
+
}).pipe(Effect.asVoid),
|
|
27
|
+
|
|
28
|
+
delete: (key: string) =>
|
|
29
|
+
Effect.tryPromise({
|
|
30
|
+
try: () => redis.del(key),
|
|
31
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
32
|
+
}).pipe(Effect.asVoid),
|
|
33
|
+
|
|
34
|
+
list: (keyPrefix: string) =>
|
|
35
|
+
Effect.gen(function* (_) {
|
|
36
|
+
const keys = new Set<string>();
|
|
37
|
+
let cursor: RedisArgument = "0";
|
|
38
|
+
|
|
39
|
+
do {
|
|
40
|
+
const result = yield* _(
|
|
41
|
+
Effect.tryPromise({
|
|
42
|
+
try: () =>
|
|
43
|
+
redis.scan(cursor, {
|
|
44
|
+
MATCH: `${keyPrefix}*`,
|
|
45
|
+
COUNT: 20,
|
|
46
|
+
}),
|
|
47
|
+
catch: (cause) =>
|
|
48
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
cursor = result.cursor;
|
|
53
|
+
for (const key of result.keys) {
|
|
54
|
+
keys.add(key.replace(keyPrefix, ""));
|
|
55
|
+
}
|
|
56
|
+
} while (cursor !== "0");
|
|
57
|
+
|
|
58
|
+
return Array.from(keys);
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Base store layer
|
|
64
|
+
export const redisKvStore = (config: RedisKvStoreConfig) =>
|
|
65
|
+
Layer.succeed(BaseKvStoreService, makeRedisBaseKvStore(config));
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@uploadista/typescript-config/server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": "./",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"]
|
|
7
|
+
},
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"typeRoots": ["../../../../node_modules/@types"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/redis-kv-store.ts"],"version":"5.9.3"}
|