@uploadista/kv-store-ioredis 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/.turbo/turbo-lint.log +0 -0
- package/LICENSE +21 -0
- package/README.md +576 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/io-redis-kv-store.d.ts +9 -0
- package/dist/io-redis-kv-store.d.ts.map +1 -0
- package/dist/io-redis-kv-store.js +37 -0
- package/package.json +29 -0
- package/src/index.ts +1 -0
- package/src/io-redis-kv-store.ts +61 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
|
File without changes
|
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,576 @@
|
|
|
1
|
+
# @uploadista/kv-store-ioredis
|
|
2
|
+
|
|
3
|
+
IORedis-backed key-value store for Uploadista. Provides advanced Redis features including clustering, Sentinel support, and connection pooling.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The IORedis KV store uses the `ioredis` library, offering:
|
|
8
|
+
|
|
9
|
+
- **Advanced Clustering**: Built-in Redis cluster support with auto-discovery
|
|
10
|
+
- **Sentinel Support**: Automatic failover with Redis Sentinel
|
|
11
|
+
- **Connection Pooling**: Efficient connection management
|
|
12
|
+
- **Lua Scripting**: Support for atomic multi-step operations
|
|
13
|
+
- **Cluster-Ready**: Production-grade cluster operations
|
|
14
|
+
- **Better Error Handling**: More granular retry and connection strategies
|
|
15
|
+
|
|
16
|
+
Compared to the standard Redis client, IORedis is optimized for complex deployments and high-availability setups.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @uploadista/kv-store-ioredis ioredis
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @uploadista/kv-store-ioredis ioredis
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Prerequisites
|
|
27
|
+
|
|
28
|
+
- Node.js 18+
|
|
29
|
+
- Redis 5.0+ or Redis cluster
|
|
30
|
+
- Optional: Redis Sentinel for automatic failover
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
36
|
+
import Redis from "ioredis";
|
|
37
|
+
import { Effect } from "effect";
|
|
38
|
+
|
|
39
|
+
// Create IORedis instance
|
|
40
|
+
const redis = new Redis({
|
|
41
|
+
host: "localhost",
|
|
42
|
+
port: 6379,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Use the KV store layer
|
|
46
|
+
const program = Effect.gen(function* () {
|
|
47
|
+
// The ioRedisKvStore is automatically available
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
Effect.runSync(
|
|
51
|
+
program.pipe(
|
|
52
|
+
Effect.provide(ioRedisKvStore({ redis }))
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- ✅ **Redis Clustering**: Built-in support for Redis clusters
|
|
60
|
+
- ✅ **Sentinel Support**: Automatic master failover detection
|
|
61
|
+
- ✅ **Connection Pooling**: Efficient resource management
|
|
62
|
+
- ✅ **Cluster Replica Reads**: Distribute read load across replicas
|
|
63
|
+
- ✅ **Auto-Reconnect**: Robust connection recovery
|
|
64
|
+
- ✅ **Pub/Sub Support**: Integration with event systems
|
|
65
|
+
- ✅ **Lua Scripting**: Advanced atomic operations
|
|
66
|
+
- ✅ **Type Safe**: Full TypeScript support
|
|
67
|
+
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### Main Exports
|
|
71
|
+
|
|
72
|
+
#### `ioRedisKvStore(config: IoRedisKvStoreConfig): Layer<BaseKvStoreService>`
|
|
73
|
+
|
|
74
|
+
Creates an Effect layer providing the `BaseKvStoreService` backed by IORedis.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
78
|
+
import Redis from "ioredis";
|
|
79
|
+
|
|
80
|
+
const redis = new Redis({ host: "localhost", port: 6379 });
|
|
81
|
+
|
|
82
|
+
const layer = ioRedisKvStore({ redis });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Configuration**:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
type IoRedisKvStoreConfig = {
|
|
89
|
+
redis: Redis; // Connected IORedis instance
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `makeIoRedisBaseKvStore(config: IoRedisKvStoreConfig): BaseKvStore`
|
|
94
|
+
|
|
95
|
+
Factory function for creating a KV store with an existing IORedis instance.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { makeIoRedisBaseKvStore } from "@uploadista/kv-store-ioredis";
|
|
99
|
+
import Redis from "ioredis";
|
|
100
|
+
|
|
101
|
+
const redis = new Redis();
|
|
102
|
+
const store = makeIoRedisBaseKvStore({ redis });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Available Operations
|
|
106
|
+
|
|
107
|
+
The IORedis store implements the `BaseKvStore` interface:
|
|
108
|
+
|
|
109
|
+
#### `get(key: string): Effect<string | null>`
|
|
110
|
+
|
|
111
|
+
Retrieve a value by key.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const program = Effect.gen(function* () {
|
|
115
|
+
const value = yield* store.get("upload:123");
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### `set(key: string, value: string): Effect<void>`
|
|
120
|
+
|
|
121
|
+
Store a string value.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const program = Effect.gen(function* () {
|
|
125
|
+
yield* store.set("upload:123", JSON.stringify(data));
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `delete(key: string): Effect<void>`
|
|
130
|
+
|
|
131
|
+
Remove a key.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const program = Effect.gen(function* () {
|
|
135
|
+
yield* store.delete("upload:123");
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `list(keyPrefix: string): Effect<string[]>`
|
|
140
|
+
|
|
141
|
+
List keys matching a prefix using SCAN (cluster-aware).
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const program = Effect.gen(function* () {
|
|
145
|
+
const keys = yield* store.list("upload:");
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
### Single Instance
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
155
|
+
import Redis from "ioredis";
|
|
156
|
+
|
|
157
|
+
const redis = new Redis({
|
|
158
|
+
host: "localhost",
|
|
159
|
+
port: 6379,
|
|
160
|
+
password: process.env.REDIS_PASSWORD,
|
|
161
|
+
db: 0,
|
|
162
|
+
retryStrategy: (times: number) => Math.min(times * 50, 2000),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const layer = ioRedisKvStore({ redis });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Redis Cluster
|
|
169
|
+
|
|
170
|
+
With automatic cluster discovery:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
174
|
+
import Redis from "ioredis";
|
|
175
|
+
|
|
176
|
+
const redis = new Redis.Cluster(
|
|
177
|
+
[
|
|
178
|
+
{ host: "node1", port: 6379 },
|
|
179
|
+
{ host: "node2", port: 6379 },
|
|
180
|
+
{ host: "node3", port: 6379 },
|
|
181
|
+
],
|
|
182
|
+
{
|
|
183
|
+
dnsLookup: (address, callback) => callback(null, address),
|
|
184
|
+
redisOptions: {
|
|
185
|
+
password: process.env.REDIS_PASSWORD,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const layer = ioRedisKvStore({ redis: redis as any });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Redis Sentinel
|
|
194
|
+
|
|
195
|
+
For high availability with automatic failover:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
199
|
+
import Redis from "ioredis";
|
|
200
|
+
|
|
201
|
+
const redis = new Redis({
|
|
202
|
+
sentinels: [
|
|
203
|
+
{ host: "sentinel1", port: 26379 },
|
|
204
|
+
{ host: "sentinel2", port: 26379 },
|
|
205
|
+
{ host: "sentinel3", port: 26379 },
|
|
206
|
+
],
|
|
207
|
+
name: "mymaster",
|
|
208
|
+
sentinelPassword: process.env.SENTINEL_PASSWORD,
|
|
209
|
+
password: process.env.REDIS_PASSWORD,
|
|
210
|
+
sentinelRetryStrategy: (times: number) => {
|
|
211
|
+
const delay = Math.min(times * 10, 1000);
|
|
212
|
+
return delay;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const layer = ioRedisKvStore({ redis });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Replica Reads
|
|
220
|
+
|
|
221
|
+
Distribute read load across replicas:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import Redis from "ioredis";
|
|
225
|
+
|
|
226
|
+
const redis = new Redis.Cluster(
|
|
227
|
+
[
|
|
228
|
+
{ host: "master", port: 6379 },
|
|
229
|
+
{ host: "replica1", port: 6379 },
|
|
230
|
+
{ host: "replica2", port: 6379 },
|
|
231
|
+
],
|
|
232
|
+
{
|
|
233
|
+
enableReadyCheck: true,
|
|
234
|
+
enableOfflineQueue: true,
|
|
235
|
+
scaleReads: "slave", // Read from replicas
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Examples
|
|
241
|
+
|
|
242
|
+
### Example 1: Distributed Server with Clustering
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
246
|
+
import { uploadServer } from "@uploadista/server";
|
|
247
|
+
import Redis from "ioredis";
|
|
248
|
+
import { Effect } from "effect";
|
|
249
|
+
|
|
250
|
+
// Cluster configuration
|
|
251
|
+
const redis = new Redis.Cluster(
|
|
252
|
+
[
|
|
253
|
+
{ host: "redis1", port: 6379 },
|
|
254
|
+
{ host: "redis2", port: 6379 },
|
|
255
|
+
{ host: "redis3", port: 6379 },
|
|
256
|
+
],
|
|
257
|
+
{
|
|
258
|
+
redisOptions: {
|
|
259
|
+
password: process.env.REDIS_PASSWORD,
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const program = Effect.gen(function* () {
|
|
265
|
+
const server = yield* uploadServer;
|
|
266
|
+
|
|
267
|
+
// Handle uploads across cluster
|
|
268
|
+
const upload = yield* server.createUpload(
|
|
269
|
+
{ filename: "large-file.zip", size: 104857600 },
|
|
270
|
+
"client:123"
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
console.log(`Upload created: ${upload.id}`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
Effect.runSync(
|
|
277
|
+
program.pipe(
|
|
278
|
+
Effect.provide(ioRedisKvStore({ redis: redis as any }))
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Example 2: Sentinel-Based High Availability
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
287
|
+
import Redis from "ioredis";
|
|
288
|
+
import { Effect } from "effect";
|
|
289
|
+
|
|
290
|
+
const redis = new Redis({
|
|
291
|
+
sentinels: [
|
|
292
|
+
{ host: "sentinel1", port: 26379 },
|
|
293
|
+
{ host: "sentinel2", port: 26379 },
|
|
294
|
+
{ host: "sentinel3", port: 26379 },
|
|
295
|
+
],
|
|
296
|
+
name: "uploadista-master",
|
|
297
|
+
password: process.env.REDIS_PASSWORD,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Automatic failover is handled by IORedis
|
|
301
|
+
const store = ioRedisKvStore({ redis });
|
|
302
|
+
|
|
303
|
+
const program = Effect.gen(function* () {
|
|
304
|
+
// Operations automatically route through Sentinel
|
|
305
|
+
// If master fails, Sentinel promotes replica automatically
|
|
306
|
+
const result = yield* Effect.tryPromise({
|
|
307
|
+
try: async () => redis.ping(),
|
|
308
|
+
catch: (e) => e as Error,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
console.log(`Redis status: ${result}`);
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Example 3: Connection Pool Optimization
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { ioRedisKvStore } from "@uploadista/kv-store-ioredis";
|
|
319
|
+
import Redis from "ioredis";
|
|
320
|
+
import { Effect } from "effect";
|
|
321
|
+
|
|
322
|
+
const redis = new Redis({
|
|
323
|
+
host: "localhost",
|
|
324
|
+
port: 6379,
|
|
325
|
+
maxRetriesPerRequest: 3,
|
|
326
|
+
enableReadyCheck: true,
|
|
327
|
+
enableOfflineQueue: true,
|
|
328
|
+
connectTimeout: 10000,
|
|
329
|
+
retryStrategy: (times: number) => {
|
|
330
|
+
const delay = Math.min(times * 50, 2000);
|
|
331
|
+
if (times > 10) {
|
|
332
|
+
return null; // Stop retrying
|
|
333
|
+
}
|
|
334
|
+
return delay;
|
|
335
|
+
},
|
|
336
|
+
reconnectOnError: (err: Error) => {
|
|
337
|
+
// Reconnect on all errors except AUTH errors
|
|
338
|
+
if (err.message.includes("WRONGPASS")) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const program = Effect.gen(function* () {
|
|
346
|
+
// Connection pool is managed automatically
|
|
347
|
+
const store = ioRedisKvStore({ redis });
|
|
348
|
+
// Use store...
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Performance Tuning
|
|
353
|
+
|
|
354
|
+
### Cluster Mode Optimization
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
const redis = new Redis.Cluster(
|
|
358
|
+
[
|
|
359
|
+
{ host: "node1", port: 6379 },
|
|
360
|
+
{ host: "node2", port: 6379 },
|
|
361
|
+
{ host: "node3", port: 6379 },
|
|
362
|
+
],
|
|
363
|
+
{
|
|
364
|
+
// Optimize cluster operations
|
|
365
|
+
maxRedirections: 16, // Cluster redirects
|
|
366
|
+
retryDelayOnFailover: 100, // Wait before retry
|
|
367
|
+
retryDelayOnClusterDown: 300, // Cluster down delay
|
|
368
|
+
dnsLookup: (address, callback) => {
|
|
369
|
+
// Implement custom DNS if needed
|
|
370
|
+
callback(null, address);
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Connection Settings
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
const redis = new Redis({
|
|
380
|
+
// Performance tuning
|
|
381
|
+
lazyConnect: false, // Connect immediately
|
|
382
|
+
enableReadyCheck: false, // Skip ready check for speed
|
|
383
|
+
enableOfflineQueue: true, // Queue commands when offline
|
|
384
|
+
maxRetriesPerRequest: 3, // Limit retries
|
|
385
|
+
socketConnectTimeout: 5000, // Socket timeout
|
|
386
|
+
socketKeepAlive: 30000, // Keep-alive
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Scaling Patterns
|
|
391
|
+
|
|
392
|
+
### Single Instance with Replica
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
Write Operations ──→ Master ──→ Replicates to ──→ Replica 1
|
|
396
|
+
Read Operations ─────────────────────────────→ Replica 2
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Redis Cluster
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
Client A ──→ Node 1 (Hash slots 0-5460)
|
|
403
|
+
Client B ──→ Node 2 (Hash slots 5461-10922)
|
|
404
|
+
Client C ──→ Node 3 (Hash slots 10923-16383)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Sentinel Setup
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
Sentinel 1 ┐
|
|
411
|
+
Sentinel 2 ├─→ Monitors ──→ Master ──→ Replica
|
|
412
|
+
Sentinel 3 ┘ ↓
|
|
413
|
+
Promotes on failure
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## Advanced Features
|
|
417
|
+
|
|
418
|
+
### Lua Scripting
|
|
419
|
+
|
|
420
|
+
For atomic operations:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import Redis from "ioredis";
|
|
424
|
+
|
|
425
|
+
const redis = new Redis();
|
|
426
|
+
|
|
427
|
+
// Atomic increment with limit
|
|
428
|
+
const script = `
|
|
429
|
+
local val = redis.call('get', KEYS[1])
|
|
430
|
+
val = (val or 0) + 1
|
|
431
|
+
if val > tonumber(ARGV[1]) then
|
|
432
|
+
return 0
|
|
433
|
+
end
|
|
434
|
+
redis.call('set', KEYS[1], val)
|
|
435
|
+
return val
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
const result = await redis.eval(script, 1, "counter", 100);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Pub/Sub for Events
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import Redis from "ioredis";
|
|
445
|
+
|
|
446
|
+
const redis = new Redis();
|
|
447
|
+
const subscriber = new Redis();
|
|
448
|
+
|
|
449
|
+
subscriber.on("message", (channel, message) => {
|
|
450
|
+
console.log(`${channel}: ${message}`);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
subscriber.subscribe("upload-events");
|
|
454
|
+
|
|
455
|
+
// Publish from main instance
|
|
456
|
+
redis.publish("upload-events", JSON.stringify({
|
|
457
|
+
type: "upload-complete",
|
|
458
|
+
uploadId: "abc123",
|
|
459
|
+
}));
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Deployment
|
|
463
|
+
|
|
464
|
+
### Docker Compose with Cluster
|
|
465
|
+
|
|
466
|
+
```yaml
|
|
467
|
+
version: "3"
|
|
468
|
+
services:
|
|
469
|
+
app:
|
|
470
|
+
environment:
|
|
471
|
+
REDIS_CLUSTER_NODES: redis1:6379,redis2:6379,redis3:6379
|
|
472
|
+
redis1:
|
|
473
|
+
image: redis:7-alpine
|
|
474
|
+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf
|
|
475
|
+
redis2:
|
|
476
|
+
image: redis:7-alpine
|
|
477
|
+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf
|
|
478
|
+
redis3:
|
|
479
|
+
image: redis:7-alpine
|
|
480
|
+
command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Kubernetes Deployment
|
|
484
|
+
|
|
485
|
+
```yaml
|
|
486
|
+
apiVersion: apps/v1
|
|
487
|
+
kind: StatefulSet
|
|
488
|
+
metadata:
|
|
489
|
+
name: redis-cluster
|
|
490
|
+
spec:
|
|
491
|
+
serviceName: redis-cluster
|
|
492
|
+
replicas: 3
|
|
493
|
+
selector:
|
|
494
|
+
matchLabels:
|
|
495
|
+
app: redis
|
|
496
|
+
template:
|
|
497
|
+
metadata:
|
|
498
|
+
labels:
|
|
499
|
+
app: redis
|
|
500
|
+
spec:
|
|
501
|
+
containers:
|
|
502
|
+
- name: redis
|
|
503
|
+
image: redis:7-alpine
|
|
504
|
+
ports:
|
|
505
|
+
- containerPort: 6379
|
|
506
|
+
- containerPort: 16379
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Related Packages
|
|
510
|
+
|
|
511
|
+
- [@uploadista/core](../../core) - Core types
|
|
512
|
+
- [@uploadista/kv-store-redis](../redis) - Standard Redis client
|
|
513
|
+
- [@uploadista/kv-store-memory](../memory) - For development
|
|
514
|
+
- [@uploadista/event-broadcaster-ioredis](../../event-emitters/event-broadcaster-ioredis) - Event broadcasting
|
|
515
|
+
- [@uploadista/server](../../servers/server) - Upload server
|
|
516
|
+
|
|
517
|
+
## Troubleshooting
|
|
518
|
+
|
|
519
|
+
### Cluster Connection Issues
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
Error: Failed to refresh slots cache
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Solutions:
|
|
526
|
+
1. Verify cluster nodes are accessible
|
|
527
|
+
2. Check cluster configuration: `redis-cli -c cluster info`
|
|
528
|
+
3. Ensure password matches across all nodes
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
redis-cli -c cluster nodes
|
|
532
|
+
# Should show all nodes as connected
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Sentinel Detection Failed
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
Error: Cannot find master from Sentinel
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Solutions:
|
|
542
|
+
1. Verify Sentinel is running
|
|
543
|
+
2. Check Sentinel configuration: `redis-cli -p 26379 SENTINEL MASTERS`
|
|
544
|
+
3. Verify master name matches configuration
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
redis-cli -p 26379
|
|
548
|
+
> SENTINEL get-master-addr-by-name uploadista-master
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### High Memory Usage
|
|
552
|
+
|
|
553
|
+
Monitor cluster memory:
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
redis-cli -c info memory | grep used_memory
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Implement cleanup:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Set TTL on session keys
|
|
563
|
+
await redis.setex("session:user123", 3600, sessionData);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## License
|
|
567
|
+
|
|
568
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
569
|
+
|
|
570
|
+
## See Also
|
|
571
|
+
|
|
572
|
+
- [KV Stores Comparison Guide](../KV_STORES_COMPARISON.md) - Compare IORedis with other options
|
|
573
|
+
- [Server Setup Guide](../../../SERVER_SETUP.md) - IORedis in production
|
|
574
|
+
- [ioredis Documentation](https://github.com/luin/ioredis) - Official ioredis docs
|
|
575
|
+
- [Redis Cluster Guide](https://redis.io/topics/cluster-tutorial) - Redis clustering
|
|
576
|
+
- [Redis Sentinel Guide](https://redis.io/topics/sentinel) - High availability
|
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,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./io-redis-kv-store";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import type { Redis as IoRedis } from "ioredis";
|
|
4
|
+
export type IoRedisKvStoreConfig = {
|
|
5
|
+
redis: IoRedis;
|
|
6
|
+
};
|
|
7
|
+
export declare function makeIoRedisBaseKvStore({ redis, }: IoRedisKvStoreConfig): BaseKvStore;
|
|
8
|
+
export declare const ioRedisKvStore: (config: IoRedisKvStoreConfig) => Layer.Layer<BaseKvStoreService, never, never>;
|
|
9
|
+
//# sourceMappingURL=io-redis-kv-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"io-redis-kv-store.d.ts","sourceRoot":"","sources":["../src/io-redis-kv-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,IAAI,OAAO,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAGF,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,GACN,EAAE,oBAAoB,GAAG,WAAW,CA4CpC;AAGD,eAAO,MAAM,cAAc,GAAI,QAAQ,oBAAoB,kDACQ,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { BaseKvStoreService } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
// Base IORedis KV store that stores raw strings
|
|
5
|
+
export function makeIoRedisBaseKvStore({ 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 [next, batch] = yield* _(Effect.tryPromise({
|
|
24
|
+
try: () => redis.scan(cursor, "MATCH", `${keyPrefix}*`, "COUNT", "20"),
|
|
25
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
26
|
+
}));
|
|
27
|
+
cursor = next;
|
|
28
|
+
for (const key of batch) {
|
|
29
|
+
keys.add(key.replace(keyPrefix, ""));
|
|
30
|
+
}
|
|
31
|
+
} while (cursor !== "0");
|
|
32
|
+
return Array.from(keys);
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Base store layer
|
|
37
|
+
export const ioRedisKvStore = (config) => Layer.succeed(BaseKvStoreService, makeIoRedisBaseKvStore(config));
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/kv-store-ioredis",
|
|
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
|
+
"ioredis": "5.8.1",
|
|
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 "./io-redis-kv-store";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
import type { Redis as IoRedis } from "ioredis";
|
|
5
|
+
|
|
6
|
+
export type IoRedisKvStoreConfig = {
|
|
7
|
+
redis: IoRedis;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Base IORedis KV store that stores raw strings
|
|
11
|
+
export function makeIoRedisBaseKvStore({
|
|
12
|
+
redis,
|
|
13
|
+
}: IoRedisKvStoreConfig): BaseKvStore {
|
|
14
|
+
return {
|
|
15
|
+
get: (key: string) =>
|
|
16
|
+
Effect.tryPromise({
|
|
17
|
+
try: () => redis.get(key),
|
|
18
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
set: (key: string, value: string) =>
|
|
22
|
+
Effect.tryPromise({
|
|
23
|
+
try: () => redis.set(key, value),
|
|
24
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
25
|
+
}).pipe(Effect.asVoid),
|
|
26
|
+
|
|
27
|
+
delete: (key: string) =>
|
|
28
|
+
Effect.tryPromise({
|
|
29
|
+
try: () => redis.del(key),
|
|
30
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
31
|
+
}).pipe(Effect.asVoid),
|
|
32
|
+
|
|
33
|
+
list: (keyPrefix: string) =>
|
|
34
|
+
Effect.gen(function* (_) {
|
|
35
|
+
const keys = new Set<string>();
|
|
36
|
+
let cursor = "0";
|
|
37
|
+
|
|
38
|
+
do {
|
|
39
|
+
const [next, batch] = yield* _(
|
|
40
|
+
Effect.tryPromise({
|
|
41
|
+
try: () =>
|
|
42
|
+
redis.scan(cursor, "MATCH", `${keyPrefix}*`, "COUNT", "20"),
|
|
43
|
+
catch: (cause) =>
|
|
44
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
cursor = next;
|
|
49
|
+
for (const key of batch) {
|
|
50
|
+
keys.add(key.replace(keyPrefix, ""));
|
|
51
|
+
}
|
|
52
|
+
} while (cursor !== "0");
|
|
53
|
+
|
|
54
|
+
return Array.from(keys);
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Base store layer
|
|
60
|
+
export const ioRedisKvStore = (config: IoRedisKvStoreConfig) =>
|
|
61
|
+
Layer.succeed(BaseKvStoreService, makeIoRedisBaseKvStore(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/io-redis-kv-store.ts"],"version":"5.9.3"}
|