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