@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.
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > @uploadista/kv-store-ioredis@0.0.2 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/kv-stores/ioredis
4
+ > tsc -b
5
+
@@ -0,0 +1,5 @@
1
+
2
+ > @uploadista/kv-store-ioredis@ check /Users/denislaboureyras/Documents/uploadista/dev/uploadista/packages/uploadista/kv-stores/ioredis
3
+ > biome check --write ./src
4
+
5
+ Checked 2 files in 53ms. No fixes applied.
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
@@ -0,0 +1,2 @@
1
+ export * from "./io-redis-kv-store";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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"}