effect-redis 0.0.26 → 0.0.28

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/src/redis/core.ts CHANGED
@@ -1,701 +1,701 @@
1
- import { Context, Effect, Layer, type Scope, Option } from 'effect';
2
- import type {
3
- KeyOptions,
4
- RedisHashValue,
5
- RedisListValue,
6
- RedisSetMember,
7
- RedisValue,
8
- ScanOptions,
9
- ScanResult,
10
- } from '../types';
11
- import { RedisCommandError, type RedisError } from '../types';
12
- import {
13
- redisClientEffect,
14
- type RedisConnectionOptions,
15
- RedisConnection,
16
- RedisConnectionLive,
17
- } from './connection';
18
- import {
19
- createRedisCommandWithCustomError,
20
- createGenericRedisCommand,
21
- type RedisClient,
22
- } from './helpers';
23
- import { toRedisError } from './errors';
24
-
25
- // Shape interface for core Redis operations
26
- // Provides a generic use() method for executing any Redis command
27
- export interface RedisShape {
28
- readonly use: <T>(
29
- fn: (client: RedisClient) => T,
30
- ) => Effect.Effect<Awaited<T>, RedisError, never>;
31
-
32
- /**
33
- * Get value by key
34
- */
35
- readonly get: (key: string) => Effect.Effect<string | null, RedisError>;
36
-
37
- /**
38
- * Set value by key
39
- */
40
- readonly set: (
41
- key: string,
42
- value: RedisValue,
43
- options?: KeyOptions,
44
- ) => Effect.Effect<'OK' | string | null, RedisError>;
45
-
46
- /**
47
- * Delete key(s)
48
- */
49
- readonly del: (...keys: string[]) => Effect.Effect<number, RedisError>;
50
-
51
- /**
52
- * Check if key exists
53
- */
54
- readonly exists: (...keys: string[]) => Effect.Effect<number, RedisError>;
55
-
56
- /**
57
- * Set expiration for key
58
- */
59
- readonly expire: (
60
- key: string,
61
- seconds: number,
62
- ) => Effect.Effect<number, RedisError>;
63
-
64
- /**
65
- * Set expiration for key in milliseconds
66
- */
67
- readonly pexpire: (
68
- key: string,
69
- milliseconds: number,
70
- ) => Effect.Effect<number, RedisError>;
71
-
72
- /**
73
- * Get TTL for key in seconds
74
- */
75
- readonly ttl: (key: string) => Effect.Effect<number, RedisError>;
76
-
77
- /**
78
- * Get TTL for key in milliseconds
79
- */
80
- readonly pttl: (key: string) => Effect.Effect<number, RedisError>;
81
-
82
- /**
83
- * Hash operations - Set field in hash
84
- */
85
- readonly hset: (
86
- key: string,
87
- field: string,
88
- value: RedisHashValue,
89
- ) => Effect.Effect<number, RedisError>;
90
-
91
- /**
92
- * Hash operations - Get field from hash
93
- */
94
- readonly hget: (
95
- key: string,
96
- field: string,
97
- ) => Effect.Effect<string | null, RedisError>;
98
-
99
- /**
100
- * Hash operations - Get all fields and values from hash
101
- */
102
- readonly hgetall: (
103
- key: string,
104
- ) => Effect.Effect<Record<string, string>, RedisError>;
105
-
106
- /**
107
- * Hash operations - Delete field(s) from hash
108
- */
109
- readonly hdel: (
110
- key: string,
111
- ...fields: string[]
112
- ) => Effect.Effect<number, RedisError>;
113
-
114
- /**
115
- * Hash operations - Check if field exists in hash
116
- */
117
- readonly hexists: (
118
- key: string,
119
- field: string,
120
- ) => Effect.Effect<boolean, RedisError>;
121
-
122
- /**
123
- * Hash operations - Get all fields from hash
124
- */
125
- readonly hkeys: (key: string) => Effect.Effect<string[], RedisError>;
126
-
127
- /**
128
- * Hash operations - Get all values from hash
129
- */
130
- readonly hvals: (key: string) => Effect.Effect<string[], RedisError>;
131
-
132
- /**
133
- * Hash operations - Get number of fields in hash
134
- */
135
- readonly hlen: (key: string) => Effect.Effect<number, RedisError>;
136
-
137
- /**
138
- * List operations - Push value(s) to left of list
139
- */
140
- readonly lpush: (
141
- key: string,
142
- ...values: RedisListValue[]
143
- ) => Effect.Effect<number, RedisError>;
144
-
145
- /**
146
- * List operations - Push value(s) to right of list
147
- */
148
- readonly rpush: (
149
- key: string,
150
- ...values: RedisListValue[]
151
- ) => Effect.Effect<number, RedisError>;
152
-
153
- /**
154
- * List operations - Pop value from left of list
155
- */
156
- readonly lpop: (
157
- key: string,
158
- count?: number | undefined,
159
- ) => Effect.Effect<string | string[] | null, RedisError>;
160
-
161
- /**
162
- * List operations - Pop value from right of list
163
- */
164
- readonly rpop: (
165
- key: string,
166
- count?: number | undefined,
167
- ) => Effect.Effect<string | string[] | null, RedisError>;
168
-
169
- /**
170
- * List operations - Get range of elements from list
171
- */
172
- readonly lrange: (
173
- key: string,
174
- start: number,
175
- stop: number,
176
- ) => Effect.Effect<string[], RedisError>;
177
-
178
- /**
179
- * List operations - Get length of list
180
- */
181
- readonly llen: (key: string) => Effect.Effect<number, RedisError>;
182
-
183
- /**
184
- * List operations - Remove elements from list
185
- */
186
- readonly lrem: (
187
- key: string,
188
- count: number,
189
- element: RedisListValue,
190
- ) => Effect.Effect<number, RedisError>;
191
-
192
- /**
193
- * Set operations - Add member(s) to set
194
- */
195
- readonly sadd: (
196
- key: string,
197
- ...members: RedisSetMember[]
198
- ) => Effect.Effect<number, RedisError>;
199
-
200
- /**
201
- * Set operations - Remove member(s) from set
202
- */
203
- readonly srem: (
204
- key: string,
205
- ...members: RedisSetMember[]
206
- ) => Effect.Effect<number, RedisError>;
207
-
208
- /**
209
- * Set operations - Check if member is in set
210
- */
211
- readonly sismember: (
212
- key: string,
213
- member: RedisSetMember,
214
- ) => Effect.Effect<boolean, RedisError>;
215
-
216
- /**
217
- * Set operations - Get all members of set
218
- */
219
- readonly smembers: (key: string) => Effect.Effect<string[], RedisError>;
220
-
221
- /**
222
- * Set operations - Get number of members in set
223
- */
224
- readonly scard: (key: string) => Effect.Effect<number, RedisError>;
225
-
226
- /**
227
- * Sorted Set operations - Add member(s) to sorted set
228
- */
229
- readonly zadd: (
230
- key: string,
231
- score: number,
232
- member: string,
233
- ...rest: Array<number | string>
234
- ) => Effect.Effect<number, RedisError>;
235
-
236
- /**
237
- * Sorted Set operations - Get range of members from sorted set
238
- */
239
- readonly zrange: (
240
- key: string,
241
- start: number,
242
- stop: number,
243
- withScores?: boolean | undefined,
244
- ) => Effect.Effect<string[] | { value: string; score: number }[], RedisError>;
245
-
246
- /**
247
- * Sorted Set operations - Get range of members by score
248
- */
249
- readonly zrangebyscore: (
250
- key: string,
251
- min: number | string,
252
- max: number | string,
253
- withScores?: boolean | undefined,
254
- ) => Effect.Effect<string[] | { value: string; score: number }[], RedisError>;
255
-
256
- /**
257
- * Sorted Set operations - Get score of member
258
- */
259
- readonly zscore: (
260
- key: string,
261
- member: string,
262
- ) => Effect.Effect<number | null, RedisError>;
263
-
264
- /**
265
- * Sorted Set operations - Remove member(s) from sorted set
266
- */
267
- readonly zrem: (
268
- key: string,
269
- ...members: string[]
270
- ) => Effect.Effect<number, RedisError>;
271
-
272
- /**
273
- * Sorted Set operations - Get number of members in sorted set
274
- */
275
- readonly zcard: (key: string) => Effect.Effect<number, RedisError>;
276
-
277
- /**
278
- * Increment value
279
- */
280
- readonly incr: (key: string) => Effect.Effect<number, RedisError>;
281
-
282
- /**
283
- * Decrement value
284
- */
285
- readonly decr: (key: string) => Effect.Effect<number, RedisError>;
286
-
287
- /**
288
- * Increment value by amount
289
- */
290
- readonly incrby: (
291
- key: string,
292
- increment: number,
293
- ) => Effect.Effect<number, RedisError>;
294
-
295
- /**
296
- * Decrement value by amount
297
- */
298
- readonly decrby: (
299
- key: string,
300
- decrement: number,
301
- ) => Effect.Effect<number, RedisError>;
302
-
303
- /**
304
- * Scan keys
305
- */
306
- readonly scan: (
307
- options?: ScanOptions | undefined,
308
- ) => Effect.Effect<ScanResult, RedisError>;
309
-
310
- /**
311
- * Flush database
312
- */
313
- readonly flushdb: () => Effect.Effect<'OK', RedisError>;
314
-
315
- /**
316
- * Flush all databases
317
- */
318
- readonly flushall: () => Effect.Effect<'OK', RedisError>;
319
-
320
- /**
321
- * Get database size
322
- */
323
- readonly dbsize: () => Effect.Effect<number, RedisError>;
324
-
325
- /**
326
- * Ping Redis
327
- */
328
- readonly ping: (
329
- message?: string | undefined,
330
- ) => Effect.Effect<string, RedisError>;
331
-
332
- /**
333
- * Execute raw Redis command
334
- */
335
- readonly execute: <A>(
336
- command: string,
337
- ...args: (string | number | Buffer)[]
338
- ) => Effect.Effect<A, RedisError>;
339
-
340
- /**
341
- * Multi/exec transaction
342
- */
343
- readonly multi: (
344
- commands: Array<[string, ...(string | number | Buffer)[]]>,
345
- ) => Effect.Effect<unknown[], RedisError>;
346
-
347
- /**
348
- * Close connection
349
- */
350
- readonly quit: () => Effect.Effect<'OK', RedisError>;
351
-
352
- /**
353
- * Force close connection
354
- */
355
- readonly disconnect: () => Effect.Effect<void, RedisError>;
356
- }
357
-
358
- // Context tag for core Redis service
359
- // Used to access Redis client through dependency injection
360
- export class Redis extends Context.Tag('Redis')<Redis, RedisShape>() {}
361
-
362
- // Bootstrap effect for creating the core Redis service
363
- // Provides the generic use() method for executing Redis commands
364
- const bootstrapRedisServiceEffect: Effect.Effect<
365
- RedisShape,
366
- RedisError,
367
- Scope.Scope | RedisConnectionOptions | RedisConnection
368
- > = Effect.gen(function* () {
369
- // Get a managed Redis client
370
- const typedClient: RedisClient = yield* Effect.serviceOption(
371
- RedisConnection,
372
- ).pipe(
373
- Effect.flatMap((opt) =>
374
- Option.isSome(opt) ? Effect.succeed(opt.value) : redisClientEffect,
375
- ),
376
- );
377
-
378
- return Redis.of({
379
- // Generic method to execute any Redis command with proper error handling
380
- use: <T>(fn: (client: import('./helpers').RedisClient) => T) =>
381
- createGenericRedisCommand(typedClient, fn) as Effect.Effect<
382
- Awaited<T>,
383
- RedisError,
384
- never
385
- >,
386
-
387
- get: createRedisCommandWithCustomError((key: string) =>
388
- typedClient.get(key),
389
- ),
390
-
391
- set: (key: string, value: RedisValue, options?: KeyOptions) =>
392
- createRedisCommandWithCustomError(
393
- (key: string, value: RedisValue, options?: KeyOptions) => {
394
- if (!options) {
395
- return typedClient.set(key, value);
396
- }
397
-
398
- const setOptions: Record<string, unknown> = {};
399
-
400
- if (options.expiration) {
401
- setOptions[options.expiration.mode] = options.expiration.time;
402
- }
403
-
404
- if (options.condition) {
405
- setOptions[options.condition] = true;
406
- }
407
-
408
- if (options.get) {
409
- setOptions.GET = true;
410
- }
411
-
412
- return Object.keys(setOptions).length > 0
413
- ? typedClient.set(key, value, setOptions)
414
- : typedClient.set(key, value);
415
- },
416
- )(key, value, options),
417
-
418
- del: createRedisCommandWithCustomError((...keys: string[]) =>
419
- typedClient.del(keys),
420
- ),
421
-
422
- exists: createRedisCommandWithCustomError((...keys: string[]) =>
423
- typedClient.exists(keys),
424
- ),
425
-
426
- expire: createRedisCommandWithCustomError((key: string, seconds: number) =>
427
- typedClient.expire(key, seconds),
428
- ),
429
-
430
- ttl: createRedisCommandWithCustomError((key: string) =>
431
- typedClient.ttl(key),
432
- ),
433
-
434
- pexpire: createRedisCommandWithCustomError(
435
- (key: string, milliseconds: number) =>
436
- typedClient.pExpire(key, milliseconds),
437
- ),
438
-
439
- pttl: createRedisCommandWithCustomError((key: string) =>
440
- typedClient.pTTL(key),
441
- ),
442
-
443
- hset: createRedisCommandWithCustomError(
444
- (key: string, field: string, value: RedisHashValue) =>
445
- typedClient.hSet(key, field, value as string),
446
- ),
447
-
448
- hget: createRedisCommandWithCustomError((key: string, field: string) =>
449
- typedClient.hGet(key, field),
450
- ),
451
-
452
- hgetall: createRedisCommandWithCustomError((key: string) =>
453
- typedClient.hGetAll(key),
454
- ),
455
-
456
- hdel: createRedisCommandWithCustomError(
457
- (key: string, ...fields: string[]) => typedClient.hDel(key, fields),
458
- ),
459
-
460
- hexists: createRedisCommandWithCustomError(
461
- async (key: string, field: string) =>
462
- (await typedClient.hExists(key, field)) === 1,
463
- ),
464
-
465
- hkeys: createRedisCommandWithCustomError((key: string) =>
466
- typedClient.hKeys(key),
467
- ),
468
-
469
- hvals: createRedisCommandWithCustomError((key: string) =>
470
- typedClient.hVals(key),
471
- ),
472
-
473
- hlen: createRedisCommandWithCustomError((key: string) =>
474
- typedClient.hLen(key),
475
- ),
476
-
477
- lpush: createRedisCommandWithCustomError(
478
- (key: string, ...values: RedisListValue[]) =>
479
- typedClient.lPush(key, values as string[]),
480
- ),
481
-
482
- rpush: createRedisCommandWithCustomError(
483
- (key: string, ...values: RedisListValue[]) =>
484
- typedClient.rPush(key, values as string[]),
485
- ),
486
-
487
- lpop: createRedisCommandWithCustomError(
488
- async (key: string, count?: number) => {
489
- if (count !== undefined) {
490
- return typedClient.lPopCount(key, count);
491
- }
492
- return typedClient.lPop(key);
493
- },
494
- ) as (
495
- key: string,
496
- count?: number,
497
- ) => Effect.Effect<string | string[] | null, RedisError>,
498
-
499
- rpop: createRedisCommandWithCustomError(
500
- async (key: string, count?: number) => {
501
- if (count !== undefined) {
502
- return typedClient.rPopCount(key, count);
503
- }
504
- return typedClient.rPop(key);
505
- },
506
- ) as (
507
- key: string,
508
- count?: number,
509
- ) => Effect.Effect<string | string[] | null, RedisError>,
510
-
511
- lrange: createRedisCommandWithCustomError(
512
- (key: string, start: number, stop: number) =>
513
- typedClient.lRange(key, start, stop),
514
- ),
515
-
516
- llen: createRedisCommandWithCustomError((key: string) =>
517
- typedClient.lLen(key),
518
- ),
519
-
520
- lrem: createRedisCommandWithCustomError(
521
- (key: string, count: number, element: RedisListValue) =>
522
- typedClient.lRem(key, count, element as string),
523
- ),
524
-
525
- sadd: createRedisCommandWithCustomError(
526
- (key: string, ...members: RedisSetMember[]) =>
527
- typedClient.sAdd(key, members as string[]),
528
- ),
529
-
530
- srem: createRedisCommandWithCustomError(
531
- (key: string, ...members: RedisSetMember[]) =>
532
- typedClient.sRem(key, members as string[]),
533
- ),
534
-
535
- sismember: createRedisCommandWithCustomError(
536
- async (key: string, member: RedisSetMember) =>
537
- (await typedClient.sIsMember(key, member as string)) === 1,
538
- ),
539
-
540
- smembers: createRedisCommandWithCustomError((key: string) =>
541
- typedClient.sMembers(key),
542
- ),
543
-
544
- scard: createRedisCommandWithCustomError((key: string) =>
545
- typedClient.sCard(key),
546
- ),
547
-
548
- zadd: (
549
- key: string,
550
- score: number,
551
- member: string,
552
- ...rest: Array<number | string>
553
- ) => {
554
- if (rest.length % 2 !== 0) {
555
- return Effect.fail(
556
- new RedisCommandError({
557
- cause: new Error(
558
- 'Invalid zadd arguments: score-member pairs required',
559
- ),
560
- message: 'zadd requires score-member pairs',
561
- command: 'ZADD',
562
- }),
563
- );
564
- }
565
-
566
- const members: Array<{ score: number; value: string }> = [
567
- { score, value: member },
568
- ];
569
- for (let i = 0; i < rest.length; i += 2) {
570
- members.push({
571
- score: rest[i] as number,
572
- value: rest[i + 1] as string,
573
- });
574
- }
575
-
576
- return Effect.tryPromise({
577
- try: () => typedClient.zAdd(key, members),
578
- catch: toRedisError,
579
- });
580
- },
581
-
582
- zrange: createRedisCommandWithCustomError(
583
- async (
584
- key: string,
585
- start: number,
586
- stop: number,
587
- withScores?: boolean,
588
- ) => {
589
- if (withScores) {
590
- return typedClient.zRangeWithScores(key, start, stop);
591
- }
592
- return typedClient.zRange(key, start, stop);
593
- },
594
- ),
595
-
596
- zrangebyscore: createRedisCommandWithCustomError(
597
- async (
598
- key: string,
599
- min: number | string,
600
- max: number | string,
601
- withScores?: boolean,
602
- ) => {
603
- if (withScores) {
604
- return typedClient.zRangeByScoreWithScores(key, min, max);
605
- }
606
- return typedClient.zRangeByScore(key, min, max);
607
- },
608
- ),
609
-
610
- zscore: createRedisCommandWithCustomError((key: string, member: string) =>
611
- typedClient.zScore(key, member),
612
- ),
613
-
614
- zrem: createRedisCommandWithCustomError(
615
- (key: string, ...members: string[]) => typedClient.zRem(key, members),
616
- ),
617
-
618
- zcard: createRedisCommandWithCustomError((key: string) =>
619
- typedClient.zCard(key),
620
- ),
621
-
622
- incr: createRedisCommandWithCustomError((key: string) =>
623
- typedClient.incr(key),
624
- ),
625
-
626
- decr: createRedisCommandWithCustomError((key: string) =>
627
- typedClient.decr(key),
628
- ),
629
-
630
- incrby: createRedisCommandWithCustomError(
631
- (key: string, increment: number) => typedClient.incrBy(key, increment),
632
- ),
633
-
634
- decrby: createRedisCommandWithCustomError(
635
- (key: string, decrement: number) => typedClient.incrBy(key, -decrement),
636
- ),
637
-
638
- scan: createRedisCommandWithCustomError(async (options?: ScanOptions) => {
639
- const scanOptions: Record<string, unknown> = {};
640
- if (options?.match) scanOptions.MATCH = options.match;
641
- if (options?.count) scanOptions.COUNT = options.count;
642
- if (options?.type) scanOptions.TYPE = options.type;
643
-
644
- const cursor = options?.cursor ?? '0';
645
- const result = await typedClient.scan(cursor, scanOptions);
646
- return {
647
- cursor: result.cursor.toString(),
648
- keys: result.keys,
649
- };
650
- }),
651
-
652
- flushdb: createRedisCommandWithCustomError(async () => {
653
- await typedClient.flushDb();
654
- return 'OK' as const;
655
- }),
656
-
657
- flushall: createRedisCommandWithCustomError(async () => {
658
- await typedClient.flushAll();
659
- return 'OK' as const;
660
- }),
661
-
662
- dbsize: createRedisCommandWithCustomError(() => typedClient.dbSize()),
663
-
664
- ping: createRedisCommandWithCustomError(async (message?: string) => {
665
- const result = await (message
666
- ? typedClient.ping(message)
667
- : typedClient.ping());
668
- return result;
669
- }),
670
-
671
- execute: <A>(command: string, ...args: (string | number | Buffer)[]) =>
672
- createRedisCommandWithCustomError(() =>
673
- typedClient.sendCommand([command, ...args.map((a) => String(a))]),
674
- )() as Effect.Effect<A, RedisError>,
675
-
676
- multi: createRedisCommandWithCustomError(
677
- async (commands: Array<[string, ...(string | number | Buffer)[]]>) => {
678
- const multi = typedClient.multi();
679
- for (const [cmd, ...args] of commands) {
680
- multi.addCommand([cmd, ...args.map((a) => String(a))]);
681
- }
682
- return multi.exec();
683
- },
684
- ),
685
-
686
- quit: createRedisCommandWithCustomError(async () => {
687
- await typedClient.quit();
688
- return 'OK' as const;
689
- }),
690
-
691
- disconnect: createRedisCommandWithCustomError(() =>
692
- typedClient.disconnect(),
693
- ),
694
- });
695
- });
696
-
697
- // Live layer for the core Redis service
698
- // Creates a scoped service that manages the Redis client lifecycle
699
- export const RedisLive = Layer.scoped(Redis, bootstrapRedisServiceEffect).pipe(
700
- Layer.provide(RedisConnectionLive),
701
- );
1
+ import { Context, Effect, Layer, type Scope, Option } from 'effect';
2
+ import type {
3
+ KeyOptions,
4
+ RedisHashValue,
5
+ RedisListValue,
6
+ RedisSetMember,
7
+ RedisValue,
8
+ ScanOptions,
9
+ ScanResult,
10
+ } from '../types';
11
+ import { RedisCommandError, type RedisError } from '../types';
12
+ import {
13
+ redisClientEffect,
14
+ type RedisConnectionOptions,
15
+ RedisConnection,
16
+ RedisConnectionLive,
17
+ } from './connection';
18
+ import {
19
+ createRedisCommandWithCustomError,
20
+ createGenericRedisCommand,
21
+ type RedisClient,
22
+ } from './helpers';
23
+ import { toRedisError } from './errors';
24
+
25
+ // Shape interface for core Redis operations
26
+ // Provides a generic use() method for executing any Redis command
27
+ export interface RedisShape {
28
+ readonly use: <T>(
29
+ fn: (client: RedisClient) => T,
30
+ ) => Effect.Effect<Awaited<T>, RedisError, never>;
31
+
32
+ /**
33
+ * Get value by key
34
+ */
35
+ readonly get: (key: string) => Effect.Effect<string | null, RedisError>;
36
+
37
+ /**
38
+ * Set value by key
39
+ */
40
+ readonly set: (
41
+ key: string,
42
+ value: RedisValue,
43
+ options?: KeyOptions,
44
+ ) => Effect.Effect<'OK' | string | null, RedisError>;
45
+
46
+ /**
47
+ * Delete key(s)
48
+ */
49
+ readonly del: (...keys: string[]) => Effect.Effect<number, RedisError>;
50
+
51
+ /**
52
+ * Check if key exists
53
+ */
54
+ readonly exists: (...keys: string[]) => Effect.Effect<number, RedisError>;
55
+
56
+ /**
57
+ * Set expiration for key
58
+ */
59
+ readonly expire: (
60
+ key: string,
61
+ seconds: number,
62
+ ) => Effect.Effect<number, RedisError>;
63
+
64
+ /**
65
+ * Set expiration for key in milliseconds
66
+ */
67
+ readonly pexpire: (
68
+ key: string,
69
+ milliseconds: number,
70
+ ) => Effect.Effect<number, RedisError>;
71
+
72
+ /**
73
+ * Get TTL for key in seconds
74
+ */
75
+ readonly ttl: (key: string) => Effect.Effect<number, RedisError>;
76
+
77
+ /**
78
+ * Get TTL for key in milliseconds
79
+ */
80
+ readonly pttl: (key: string) => Effect.Effect<number, RedisError>;
81
+
82
+ /**
83
+ * Hash operations - Set field in hash
84
+ */
85
+ readonly hset: (
86
+ key: string,
87
+ field: string,
88
+ value: RedisHashValue,
89
+ ) => Effect.Effect<number, RedisError>;
90
+
91
+ /**
92
+ * Hash operations - Get field from hash
93
+ */
94
+ readonly hget: (
95
+ key: string,
96
+ field: string,
97
+ ) => Effect.Effect<string | null, RedisError>;
98
+
99
+ /**
100
+ * Hash operations - Get all fields and values from hash
101
+ */
102
+ readonly hgetall: (
103
+ key: string,
104
+ ) => Effect.Effect<Record<string, string>, RedisError>;
105
+
106
+ /**
107
+ * Hash operations - Delete field(s) from hash
108
+ */
109
+ readonly hdel: (
110
+ key: string,
111
+ ...fields: string[]
112
+ ) => Effect.Effect<number, RedisError>;
113
+
114
+ /**
115
+ * Hash operations - Check if field exists in hash
116
+ */
117
+ readonly hexists: (
118
+ key: string,
119
+ field: string,
120
+ ) => Effect.Effect<boolean, RedisError>;
121
+
122
+ /**
123
+ * Hash operations - Get all fields from hash
124
+ */
125
+ readonly hkeys: (key: string) => Effect.Effect<string[], RedisError>;
126
+
127
+ /**
128
+ * Hash operations - Get all values from hash
129
+ */
130
+ readonly hvals: (key: string) => Effect.Effect<string[], RedisError>;
131
+
132
+ /**
133
+ * Hash operations - Get number of fields in hash
134
+ */
135
+ readonly hlen: (key: string) => Effect.Effect<number, RedisError>;
136
+
137
+ /**
138
+ * List operations - Push value(s) to left of list
139
+ */
140
+ readonly lpush: (
141
+ key: string,
142
+ ...values: RedisListValue[]
143
+ ) => Effect.Effect<number, RedisError>;
144
+
145
+ /**
146
+ * List operations - Push value(s) to right of list
147
+ */
148
+ readonly rpush: (
149
+ key: string,
150
+ ...values: RedisListValue[]
151
+ ) => Effect.Effect<number, RedisError>;
152
+
153
+ /**
154
+ * List operations - Pop value from left of list
155
+ */
156
+ readonly lpop: (
157
+ key: string,
158
+ count?: number | undefined,
159
+ ) => Effect.Effect<string | string[] | null, RedisError>;
160
+
161
+ /**
162
+ * List operations - Pop value from right of list
163
+ */
164
+ readonly rpop: (
165
+ key: string,
166
+ count?: number | undefined,
167
+ ) => Effect.Effect<string | string[] | null, RedisError>;
168
+
169
+ /**
170
+ * List operations - Get range of elements from list
171
+ */
172
+ readonly lrange: (
173
+ key: string,
174
+ start: number,
175
+ stop: number,
176
+ ) => Effect.Effect<string[], RedisError>;
177
+
178
+ /**
179
+ * List operations - Get length of list
180
+ */
181
+ readonly llen: (key: string) => Effect.Effect<number, RedisError>;
182
+
183
+ /**
184
+ * List operations - Remove elements from list
185
+ */
186
+ readonly lrem: (
187
+ key: string,
188
+ count: number,
189
+ element: RedisListValue,
190
+ ) => Effect.Effect<number, RedisError>;
191
+
192
+ /**
193
+ * Set operations - Add member(s) to set
194
+ */
195
+ readonly sadd: (
196
+ key: string,
197
+ ...members: RedisSetMember[]
198
+ ) => Effect.Effect<number, RedisError>;
199
+
200
+ /**
201
+ * Set operations - Remove member(s) from set
202
+ */
203
+ readonly srem: (
204
+ key: string,
205
+ ...members: RedisSetMember[]
206
+ ) => Effect.Effect<number, RedisError>;
207
+
208
+ /**
209
+ * Set operations - Check if member is in set
210
+ */
211
+ readonly sismember: (
212
+ key: string,
213
+ member: RedisSetMember,
214
+ ) => Effect.Effect<boolean, RedisError>;
215
+
216
+ /**
217
+ * Set operations - Get all members of set
218
+ */
219
+ readonly smembers: (key: string) => Effect.Effect<string[], RedisError>;
220
+
221
+ /**
222
+ * Set operations - Get number of members in set
223
+ */
224
+ readonly scard: (key: string) => Effect.Effect<number, RedisError>;
225
+
226
+ /**
227
+ * Sorted Set operations - Add member(s) to sorted set
228
+ */
229
+ readonly zadd: (
230
+ key: string,
231
+ score: number,
232
+ member: string,
233
+ ...rest: Array<number | string>
234
+ ) => Effect.Effect<number, RedisError>;
235
+
236
+ /**
237
+ * Sorted Set operations - Get range of members from sorted set
238
+ */
239
+ readonly zrange: (
240
+ key: string,
241
+ start: number,
242
+ stop: number,
243
+ withScores?: boolean | undefined,
244
+ ) => Effect.Effect<string[] | { value: string; score: number }[], RedisError>;
245
+
246
+ /**
247
+ * Sorted Set operations - Get range of members by score
248
+ */
249
+ readonly zrangebyscore: (
250
+ key: string,
251
+ min: number | string,
252
+ max: number | string,
253
+ withScores?: boolean | undefined,
254
+ ) => Effect.Effect<string[] | { value: string; score: number }[], RedisError>;
255
+
256
+ /**
257
+ * Sorted Set operations - Get score of member
258
+ */
259
+ readonly zscore: (
260
+ key: string,
261
+ member: string,
262
+ ) => Effect.Effect<number | null, RedisError>;
263
+
264
+ /**
265
+ * Sorted Set operations - Remove member(s) from sorted set
266
+ */
267
+ readonly zrem: (
268
+ key: string,
269
+ ...members: string[]
270
+ ) => Effect.Effect<number, RedisError>;
271
+
272
+ /**
273
+ * Sorted Set operations - Get number of members in sorted set
274
+ */
275
+ readonly zcard: (key: string) => Effect.Effect<number, RedisError>;
276
+
277
+ /**
278
+ * Increment value
279
+ */
280
+ readonly incr: (key: string) => Effect.Effect<number, RedisError>;
281
+
282
+ /**
283
+ * Decrement value
284
+ */
285
+ readonly decr: (key: string) => Effect.Effect<number, RedisError>;
286
+
287
+ /**
288
+ * Increment value by amount
289
+ */
290
+ readonly incrby: (
291
+ key: string,
292
+ increment: number,
293
+ ) => Effect.Effect<number, RedisError>;
294
+
295
+ /**
296
+ * Decrement value by amount
297
+ */
298
+ readonly decrby: (
299
+ key: string,
300
+ decrement: number,
301
+ ) => Effect.Effect<number, RedisError>;
302
+
303
+ /**
304
+ * Scan keys
305
+ */
306
+ readonly scan: (
307
+ options?: ScanOptions | undefined,
308
+ ) => Effect.Effect<ScanResult, RedisError>;
309
+
310
+ /**
311
+ * Flush database
312
+ */
313
+ readonly flushdb: () => Effect.Effect<'OK', RedisError>;
314
+
315
+ /**
316
+ * Flush all databases
317
+ */
318
+ readonly flushall: () => Effect.Effect<'OK', RedisError>;
319
+
320
+ /**
321
+ * Get database size
322
+ */
323
+ readonly dbsize: () => Effect.Effect<number, RedisError>;
324
+
325
+ /**
326
+ * Ping Redis
327
+ */
328
+ readonly ping: (
329
+ message?: string | undefined,
330
+ ) => Effect.Effect<string, RedisError>;
331
+
332
+ /**
333
+ * Execute raw Redis command
334
+ */
335
+ readonly execute: <A>(
336
+ command: string,
337
+ ...args: (string | number | Buffer)[]
338
+ ) => Effect.Effect<A, RedisError>;
339
+
340
+ /**
341
+ * Multi/exec transaction
342
+ */
343
+ readonly multi: (
344
+ commands: Array<[string, ...(string | number | Buffer)[]]>,
345
+ ) => Effect.Effect<unknown[], RedisError>;
346
+
347
+ /**
348
+ * Close connection
349
+ */
350
+ readonly quit: () => Effect.Effect<'OK', RedisError>;
351
+
352
+ /**
353
+ * Force close connection
354
+ */
355
+ readonly disconnect: () => Effect.Effect<void, RedisError>;
356
+ }
357
+
358
+ // Context tag for core Redis service
359
+ // Used to access Redis client through dependency injection
360
+ export class Redis extends Context.Tag('Redis')<Redis, RedisShape>() {}
361
+
362
+ // Bootstrap effect for creating the core Redis service
363
+ // Provides the generic use() method for executing Redis commands
364
+ const bootstrapRedisServiceEffect: Effect.Effect<
365
+ RedisShape,
366
+ RedisError,
367
+ Scope.Scope | RedisConnectionOptions | RedisConnection
368
+ > = Effect.gen(function* () {
369
+ // Get a managed Redis client
370
+ const typedClient: RedisClient = yield* Effect.serviceOption(
371
+ RedisConnection,
372
+ ).pipe(
373
+ Effect.flatMap((opt) =>
374
+ Option.isSome(opt) ? Effect.succeed(opt.value) : redisClientEffect,
375
+ ),
376
+ );
377
+
378
+ return Redis.of({
379
+ // Generic method to execute any Redis command with proper error handling
380
+ use: <T>(fn: (client: import('./helpers').RedisClient) => T) =>
381
+ createGenericRedisCommand(typedClient, fn) as Effect.Effect<
382
+ Awaited<T>,
383
+ RedisError,
384
+ never
385
+ >,
386
+
387
+ get: createRedisCommandWithCustomError((key: string) =>
388
+ typedClient.get(key),
389
+ ),
390
+
391
+ set: (key: string, value: RedisValue, options?: KeyOptions) =>
392
+ createRedisCommandWithCustomError(
393
+ (key: string, value: RedisValue, options?: KeyOptions) => {
394
+ if (!options) {
395
+ return typedClient.set(key, value);
396
+ }
397
+
398
+ const setOptions: Record<string, unknown> = {};
399
+
400
+ if (options.expiration) {
401
+ setOptions[options.expiration.mode] = options.expiration.time;
402
+ }
403
+
404
+ if (options.condition) {
405
+ setOptions[options.condition] = true;
406
+ }
407
+
408
+ if (options.get) {
409
+ setOptions.GET = true;
410
+ }
411
+
412
+ return Object.keys(setOptions).length > 0
413
+ ? typedClient.set(key, value, setOptions)
414
+ : typedClient.set(key, value);
415
+ },
416
+ )(key, value, options),
417
+
418
+ del: createRedisCommandWithCustomError((...keys: string[]) =>
419
+ typedClient.del(keys),
420
+ ),
421
+
422
+ exists: createRedisCommandWithCustomError((...keys: string[]) =>
423
+ typedClient.exists(keys),
424
+ ),
425
+
426
+ expire: createRedisCommandWithCustomError((key: string, seconds: number) =>
427
+ typedClient.expire(key, seconds),
428
+ ),
429
+
430
+ ttl: createRedisCommandWithCustomError((key: string) =>
431
+ typedClient.ttl(key),
432
+ ),
433
+
434
+ pexpire: createRedisCommandWithCustomError(
435
+ (key: string, milliseconds: number) =>
436
+ typedClient.pExpire(key, milliseconds),
437
+ ),
438
+
439
+ pttl: createRedisCommandWithCustomError((key: string) =>
440
+ typedClient.pTTL(key),
441
+ ),
442
+
443
+ hset: createRedisCommandWithCustomError(
444
+ (key: string, field: string, value: RedisHashValue) =>
445
+ typedClient.hSet(key, field, value as string),
446
+ ),
447
+
448
+ hget: createRedisCommandWithCustomError((key: string, field: string) =>
449
+ typedClient.hGet(key, field),
450
+ ),
451
+
452
+ hgetall: createRedisCommandWithCustomError((key: string) =>
453
+ typedClient.hGetAll(key),
454
+ ),
455
+
456
+ hdel: createRedisCommandWithCustomError(
457
+ (key: string, ...fields: string[]) => typedClient.hDel(key, fields),
458
+ ),
459
+
460
+ hexists: createRedisCommandWithCustomError(
461
+ async (key: string, field: string) =>
462
+ (await typedClient.hExists(key, field)) === 1,
463
+ ),
464
+
465
+ hkeys: createRedisCommandWithCustomError((key: string) =>
466
+ typedClient.hKeys(key),
467
+ ),
468
+
469
+ hvals: createRedisCommandWithCustomError((key: string) =>
470
+ typedClient.hVals(key),
471
+ ),
472
+
473
+ hlen: createRedisCommandWithCustomError((key: string) =>
474
+ typedClient.hLen(key),
475
+ ),
476
+
477
+ lpush: createRedisCommandWithCustomError(
478
+ (key: string, ...values: RedisListValue[]) =>
479
+ typedClient.lPush(key, values as string[]),
480
+ ),
481
+
482
+ rpush: createRedisCommandWithCustomError(
483
+ (key: string, ...values: RedisListValue[]) =>
484
+ typedClient.rPush(key, values as string[]),
485
+ ),
486
+
487
+ lpop: createRedisCommandWithCustomError(
488
+ async (key: string, count?: number) => {
489
+ if (count !== undefined) {
490
+ return typedClient.lPopCount(key, count);
491
+ }
492
+ return typedClient.lPop(key);
493
+ },
494
+ ) as (
495
+ key: string,
496
+ count?: number,
497
+ ) => Effect.Effect<string | string[] | null, RedisError>,
498
+
499
+ rpop: createRedisCommandWithCustomError(
500
+ async (key: string, count?: number) => {
501
+ if (count !== undefined) {
502
+ return typedClient.rPopCount(key, count);
503
+ }
504
+ return typedClient.rPop(key);
505
+ },
506
+ ) as (
507
+ key: string,
508
+ count?: number,
509
+ ) => Effect.Effect<string | string[] | null, RedisError>,
510
+
511
+ lrange: createRedisCommandWithCustomError(
512
+ (key: string, start: number, stop: number) =>
513
+ typedClient.lRange(key, start, stop),
514
+ ),
515
+
516
+ llen: createRedisCommandWithCustomError((key: string) =>
517
+ typedClient.lLen(key),
518
+ ),
519
+
520
+ lrem: createRedisCommandWithCustomError(
521
+ (key: string, count: number, element: RedisListValue) =>
522
+ typedClient.lRem(key, count, element as string),
523
+ ),
524
+
525
+ sadd: createRedisCommandWithCustomError(
526
+ (key: string, ...members: RedisSetMember[]) =>
527
+ typedClient.sAdd(key, members as string[]),
528
+ ),
529
+
530
+ srem: createRedisCommandWithCustomError(
531
+ (key: string, ...members: RedisSetMember[]) =>
532
+ typedClient.sRem(key, members as string[]),
533
+ ),
534
+
535
+ sismember: createRedisCommandWithCustomError(
536
+ async (key: string, member: RedisSetMember) =>
537
+ (await typedClient.sIsMember(key, member as string)) === 1,
538
+ ),
539
+
540
+ smembers: createRedisCommandWithCustomError((key: string) =>
541
+ typedClient.sMembers(key),
542
+ ),
543
+
544
+ scard: createRedisCommandWithCustomError((key: string) =>
545
+ typedClient.sCard(key),
546
+ ),
547
+
548
+ zadd: (
549
+ key: string,
550
+ score: number,
551
+ member: string,
552
+ ...rest: Array<number | string>
553
+ ) => {
554
+ if (rest.length % 2 !== 0) {
555
+ return Effect.fail(
556
+ new RedisCommandError({
557
+ cause: new Error(
558
+ 'Invalid zadd arguments: score-member pairs required',
559
+ ),
560
+ message: 'zadd requires score-member pairs',
561
+ command: 'ZADD',
562
+ }),
563
+ );
564
+ }
565
+
566
+ const members: Array<{ score: number; value: string }> = [
567
+ { score, value: member },
568
+ ];
569
+ for (let i = 0; i < rest.length; i += 2) {
570
+ members.push({
571
+ score: rest[i] as number,
572
+ value: rest[i + 1] as string,
573
+ });
574
+ }
575
+
576
+ return Effect.tryPromise({
577
+ try: () => typedClient.zAdd(key, members),
578
+ catch: toRedisError,
579
+ });
580
+ },
581
+
582
+ zrange: createRedisCommandWithCustomError(
583
+ async (
584
+ key: string,
585
+ start: number,
586
+ stop: number,
587
+ withScores?: boolean,
588
+ ) => {
589
+ if (withScores) {
590
+ return typedClient.zRangeWithScores(key, start, stop);
591
+ }
592
+ return typedClient.zRange(key, start, stop);
593
+ },
594
+ ),
595
+
596
+ zrangebyscore: createRedisCommandWithCustomError(
597
+ async (
598
+ key: string,
599
+ min: number | string,
600
+ max: number | string,
601
+ withScores?: boolean,
602
+ ) => {
603
+ if (withScores) {
604
+ return typedClient.zRangeByScoreWithScores(key, min, max);
605
+ }
606
+ return typedClient.zRangeByScore(key, min, max);
607
+ },
608
+ ),
609
+
610
+ zscore: createRedisCommandWithCustomError((key: string, member: string) =>
611
+ typedClient.zScore(key, member),
612
+ ),
613
+
614
+ zrem: createRedisCommandWithCustomError(
615
+ (key: string, ...members: string[]) => typedClient.zRem(key, members),
616
+ ),
617
+
618
+ zcard: createRedisCommandWithCustomError((key: string) =>
619
+ typedClient.zCard(key),
620
+ ),
621
+
622
+ incr: createRedisCommandWithCustomError((key: string) =>
623
+ typedClient.incr(key),
624
+ ),
625
+
626
+ decr: createRedisCommandWithCustomError((key: string) =>
627
+ typedClient.decr(key),
628
+ ),
629
+
630
+ incrby: createRedisCommandWithCustomError(
631
+ (key: string, increment: number) => typedClient.incrBy(key, increment),
632
+ ),
633
+
634
+ decrby: createRedisCommandWithCustomError(
635
+ (key: string, decrement: number) => typedClient.incrBy(key, -decrement),
636
+ ),
637
+
638
+ scan: createRedisCommandWithCustomError(async (options?: ScanOptions) => {
639
+ const scanOptions: Record<string, unknown> = {};
640
+ if (options?.match) scanOptions.MATCH = options.match;
641
+ if (options?.count) scanOptions.COUNT = options.count;
642
+ if (options?.type) scanOptions.TYPE = options.type;
643
+
644
+ const cursor = options?.cursor ?? '0';
645
+ const result = await typedClient.scan(cursor, scanOptions);
646
+ return {
647
+ cursor: result.cursor.toString(),
648
+ keys: result.keys,
649
+ };
650
+ }),
651
+
652
+ flushdb: createRedisCommandWithCustomError(async () => {
653
+ await typedClient.flushDb();
654
+ return 'OK' as const;
655
+ }),
656
+
657
+ flushall: createRedisCommandWithCustomError(async () => {
658
+ await typedClient.flushAll();
659
+ return 'OK' as const;
660
+ }),
661
+
662
+ dbsize: createRedisCommandWithCustomError(() => typedClient.dbSize()),
663
+
664
+ ping: createRedisCommandWithCustomError(async (message?: string) => {
665
+ const result = await (message
666
+ ? typedClient.ping(message)
667
+ : typedClient.ping());
668
+ return result;
669
+ }),
670
+
671
+ execute: <A>(command: string, ...args: (string | number | Buffer)[]) =>
672
+ createRedisCommandWithCustomError(() =>
673
+ typedClient.sendCommand([command, ...args.map((a) => String(a))]),
674
+ )() as Effect.Effect<A, RedisError>,
675
+
676
+ multi: createRedisCommandWithCustomError(
677
+ async (commands: Array<[string, ...(string | number | Buffer)[]]>) => {
678
+ const multi = typedClient.multi();
679
+ for (const [cmd, ...args] of commands) {
680
+ multi.addCommand([cmd, ...args.map((a) => String(a))]);
681
+ }
682
+ return multi.exec();
683
+ },
684
+ ),
685
+
686
+ quit: createRedisCommandWithCustomError(async () => {
687
+ await typedClient.quit();
688
+ return 'OK' as const;
689
+ }),
690
+
691
+ disconnect: createRedisCommandWithCustomError(() =>
692
+ typedClient.disconnect(),
693
+ ),
694
+ });
695
+ });
696
+
697
+ // Live layer for the core Redis service
698
+ // Creates a scoped service that manages the Redis client lifecycle
699
+ export const RedisLive = Layer.scoped(Redis, bootstrapRedisServiceEffect).pipe(
700
+ Layer.provide(RedisConnectionLive),
701
+ );