@envoy1084/effect-redis 0.0.0

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/README.md ADDED
@@ -0,0 +1,1077 @@
1
+ # effect-redis
2
+
3
+ > A experimental, Effect wrapper for Redis, providing type-safe, composable Redis operations with support for transactions, pipelines, and all major Redis command groups.
4
+
5
+ ## What is This?
6
+
7
+ **effect-redis** is a comprehensive Redis client wrapper built on top of [Effect](https://effect.website/) and the [redis](https://www.npmjs.com/package/redis) npm package. It provides:
8
+
9
+ - **Effect-based API**: Full integration with Effect's functional error handling, dependency injection, and compositional patterns
10
+ - **Type-safe operations**: TypeScript types for all Redis commands with proper argument and return type validation
11
+ - **Transaction support**: Built-in support for Redis `MULTI` transactions with automatic queuing
12
+ - **Pipeline support**: Batch multiple commands efficiently with automatic execution
13
+ - **Comprehensive command coverage**: Support for 11+ Redis command groups including strings, hashes, lists, sets, sorted sets, geospatial, JSON, bitmaps, HyperLogLog, scripting, and generic commands
14
+ - **Dependency-driven**: Leverages Effect's Layer system for elegant connection management and resource lifecycle handling
15
+ - **Error handling**: Unified error handling through Effect's error channel with custom `RedisError` type
16
+
17
+ ## Quick Start
18
+
19
+ ### Installation
20
+ ****
21
+ ```bash
22
+ npm install @envoy1084/effect-redis effect @effect/platform-node
23
+ # or
24
+ yarn add @envoy1084/effect-redis effect @effect/platform-node
25
+ # or
26
+ pnpm add @envoy1084/effect-redis effect @effect/platform-node
27
+ # or
28
+ bun add @envoy1084/effect-redis effect @effect/platform-node
29
+ ```
30
+
31
+ ### Basic Usage
32
+
33
+ ```typescript
34
+ import { runMain } from "@effect/platform-node/NodeRuntime";
35
+ import {
36
+ layerWithOptions,
37
+ RedisCore,
38
+ RedisCoreLive,
39
+ } from "@envoy1084/effect-redis";
40
+ import { Effect, Layer } from "effect";
41
+
42
+ // Create the Redis layer with connection options
43
+ const RedisLayer = RedisCoreLive.pipe(
44
+ Layer.provideMerge(
45
+ layerWithOptions({
46
+ url: "redis://localhost:6379",
47
+ }),
48
+ ),
49
+ );
50
+
51
+ // Define your program
52
+ const program = Effect.gen(function* () {
53
+ const redis = yield* RedisCore;
54
+
55
+ // Use redis commands
56
+ yield* redis.set("mykey", "myvalue");
57
+ const value = yield* redis.get("mykey");
58
+ yield* Effect.log(`Got value: ${value}`);
59
+ });
60
+
61
+ // Run the program
62
+ program.pipe(Effect.provide(RedisLayer), runMain);
63
+ ```
64
+
65
+ ## Connection Setup
66
+
67
+ ### Providing Layers to Your Program
68
+
69
+ effect-redis uses Effect's Layer system for dependency injection and resource management. Here's how to configure connections:
70
+
71
+ #### 1. **With Custom Connection Options**
72
+
73
+ ```typescript
74
+ import { layerWithOptions } from "@envoy1084/effect-redis";
75
+ import { Layer } from "effect";
76
+
77
+ const RedisLayer = RedisCoreLive.pipe(
78
+ Layer.provideMerge(
79
+ layerWithOptions({
80
+ url: "redis://localhost:6379",
81
+ password: "your-password",
82
+ database: 0,
83
+ // ... all RedisClientOptions from the redis package
84
+ }),
85
+ ),
86
+ );
87
+ ```
88
+
89
+ #### 2. **Multiple Redis Instances**
90
+
91
+ ```typescript
92
+ import { RedisCore, RedisCoreLive, layerWithOptions } from "@envoy1084/effect-redis";
93
+ import { Context, Layer, Effect } from "effect";
94
+
95
+ // Define tags for different Redis instances
96
+ class PrimaryRedis extends Context.Tag("PrimaryRedis")<
97
+ PrimaryRedis,
98
+ RedisCoreShape
99
+ >() {}
100
+
101
+ class CacheRedis extends Context.Tag("CacheRedis")<
102
+ CacheRedis,
103
+ RedisCoreShape
104
+ >() {}
105
+
106
+ // Create separate layers
107
+ const PrimaryRedisLayer = Layer.effect(
108
+ PrimaryRedis,
109
+ Effect.gen(function* () {
110
+ const { client } = yield* RedisConnection;
111
+ return makeRedisCore(client);
112
+ }),
113
+ ).pipe(
114
+ Layer.provideMerge(
115
+ layerWithOptions({ url: "redis://primary:6379" }),
116
+ ),
117
+ );
118
+
119
+ // Use in your program
120
+ const program = Effect.gen(function* () {
121
+ const primary = yield* PrimaryRedis;
122
+ const cache = yield* CacheRedis;
123
+ // Use both instances
124
+ });
125
+ ```
126
+
127
+ ## Usage Guide
128
+
129
+ ### String Commands
130
+
131
+ String commands operate on text values.
132
+
133
+ ```typescript
134
+ const program = Effect.gen(function* () {
135
+ const redis = yield* RedisCore;
136
+
137
+ // Basic GET/SET
138
+ yield* redis.set("name", "Alice");
139
+ const name = yield* redis.get("name");
140
+
141
+ // Multiple keys
142
+ yield* redis.mSet([
143
+ ["key1", "value1"],
144
+ ["key2", "value2"],
145
+ ]);
146
+ const values = yield* redis.mGet(["key1", "key2"]);
147
+
148
+ // Increment/Decrement
149
+ yield* redis.set("counter", "10");
150
+ yield* redis.incr("counter"); // 11
151
+ yield* redis.incrBy("counter", 5); // 16
152
+ yield* redis.decr("counter"); // 15
153
+ yield* redis.decrBy("counter", 3); // 12
154
+
155
+ // Float operations
156
+ yield* redis.set("price", "19.99");
157
+ yield* redis.incrByFloat("price", 0.01); // 20.00
158
+
159
+ // Advanced operations
160
+ yield* redis.append("name", " Smith");
161
+ const length = yield* redis.strLen("name");
162
+ const substring = yield* redis.getRange("name", 0, 4);
163
+ const oldValue = yield* redis.getSet("name", "Bob");
164
+ yield* redis.setNX("newkey", "value"); // Only if key doesn't exist
165
+ yield* redis.setEx("tempkey", 60, "value"); // With expiration
166
+ });
167
+ ```
168
+
169
+ **Supported String Commands:**
170
+ - `get`, `set`, `mGet`, `mSet`, `setNX`, `mSetNX`
171
+ - `incr`, `incrBy`, `incrByFloat`, `decr`, `decrBy`
172
+ - `append`, `strLen`, `getRange`, `setRange`
173
+ - `getSet`, `getDel`, `getEx`, `setEx`, `pSetEx`
174
+ - `getdel`, `delEx`, `digest`, `lcs`, `mSetEx`
175
+
176
+ ### Hash Commands
177
+
178
+ Hash commands operate on objects with named fields.
179
+
180
+ ```typescript
181
+ const program = Effect.gen(function* () {
182
+ const redis = yield* RedisCore;
183
+
184
+ // Basic operations
185
+ yield* redis.hSet("user:1", "name", "Alice");
186
+ yield* redis.hSet("user:1", {
187
+ "age": "30",
188
+ "email": "alice@example.com",
189
+ });
190
+
191
+ const name = yield* redis.hGet("user:1", "name");
192
+ const allFields = yield* redis.hGetAll("user:1");
193
+ const keys = yield* redis.hKeys("user:1");
194
+ const values = yield* redis.hVals("user:1");
195
+
196
+ // Check existence
197
+ const exists = yield* redis.hExists("user:1", "name");
198
+
199
+ // Increment
200
+ yield* redis.hSet("user:1", "age", "30");
201
+ yield* redis.hIncrBy("user:1", "age", 1); // 31
202
+ yield* redis.hIncrByFloat("user:1", "score", 2.5);
203
+
204
+ // Multiple operations
205
+ const fields = yield* redis.hmGet("user:1", ["name", "email"]);
206
+
207
+ // Delete and set operations
208
+ yield* redis.hSetNX("user:1", "name", "Bob"); // Won't update
209
+ yield* redis.hDel("user:1", "email");
210
+
211
+ // Expiration (Redis 7.4+)
212
+ yield* redis.hExpire("user:1", 3600, ["name", "email"]);
213
+ const ttl = yield* redis.hTTL("user:1", "name");
214
+ });
215
+ ```
216
+
217
+ **Supported Hash Commands:**
218
+ - `hGet`, `hSet`, `hGetAll`, `hmGet`, `hKeys`, `hVals`, `hLen`
219
+ - `hExists`, `hSetNX`, `hDel`, `hGetDel`
220
+ - `hIncrBy`, `hIncrByFloat`, `hStrLen`
221
+ - `hExpire`, `hExpireAt`, `hExpireTime`, `hPersist`
222
+ - `hpExpire`, `hpExpireAt`, `hpExpireTime`, `hpTTL`, `hTTL`
223
+ - `hRandField`, `hScan`, `hGetEx`, `hSetEx`
224
+
225
+ ### List Commands
226
+
227
+ List commands operate on ordered collections of strings.
228
+
229
+ ```typescript
230
+ const program = Effect.gen(function* () {
231
+ const redis = yield* RedisCore;
232
+
233
+ // Push operations
234
+ yield* redis.lPush("tasks", "task1");
235
+ yield* redis.lPush("tasks", "task2", "task3");
236
+ yield* redis.rPush("tasks", "task4");
237
+
238
+ // Pop operations
239
+ const firstTask = yield* redis.lPop("tasks");
240
+ const lastTask = yield* redis.rPop("tasks");
241
+
242
+ // Get range
243
+ const allTasks = yield* redis.lRange("tasks", 0, -1);
244
+ const midTasks = yield* redis.lRange("tasks", 0, 5);
245
+
246
+ // Check length
247
+ const count = yield* redis.lLen("tasks");
248
+
249
+ // Index operations
250
+ const taskByIndex = yield* redis.lIndex("tasks", 2);
251
+ yield* redis.lSet("tasks", 0, "updated_task");
252
+
253
+ // Insert and remove
254
+ yield* redis.lInsert("tasks", "BEFORE", "task2", "new_task");
255
+ yield* redis.lRem("tasks", 1, "old_task");
256
+ yield* redis.lTrim("tasks", 0, 10);
257
+
258
+ // Move between lists
259
+ yield* redis.lMove("tasks", "completed", "LEFT", "RIGHT");
260
+
261
+ // Blocking operations (waits for element)
262
+ const task = yield* redis.blPop(["tasks"], 30); // 30 second timeout
263
+ });
264
+ ```
265
+
266
+ **Supported List Commands:**
267
+ - `lPush`, `lPushX`, `rPush`, `rPushX`
268
+ - `lPop`, `rPop`, `lLen`, `lIndex`, `lSet`
269
+ - `lRange`, `lRem`, `lTrim`, `lInsert`, `lPos`
270
+ - `lMove`, `lMPop`, `rPopLPush`
271
+ - `blPop`, `brPop`, `brPopLPush`, `blMove`, `blmPop`
272
+
273
+ ### Set Commands
274
+
275
+ Set commands operate on unordered collections of unique strings.
276
+
277
+ ```typescript
278
+ const program = Effect.gen(function* () {
279
+ const redis = yield* RedisCore;
280
+
281
+ // Add and check members
282
+ yield* redis.sAdd("tags", "javascript", "typescript", "nodejs");
283
+ const isMember = yield* redis.sIsMember("tags", "javascript");
284
+ const allMembers = yield* redis.sMembers("tags");
285
+ const count = yield* redis.sCard("tags");
286
+
287
+ // Multiple membership check
288
+ const members = yield* redis.smIsMember("tags", [
289
+ "javascript",
290
+ "python",
291
+ ]);
292
+
293
+ // Random members
294
+ const randomMember = yield* redis.sRandMember("tags");
295
+ const randomMembers = yield* redis.sRandMember("tags", 2);
296
+
297
+ // Pop operations
298
+ const popped = yield* redis.sPop("tags");
299
+ const poppedMany = yield* redis.sPop("tags", 2);
300
+
301
+ // Set operations
302
+ const intersection = yield* redis.sInter(["tags", "languages"]);
303
+ const union = yield* redis.sUnion(["tags", "languages"]);
304
+ const difference = yield* redis.sDiff(["tags", "languages"]);
305
+
306
+ // Store operations
307
+ yield* redis.sInterStore("common_tags", ["tags", "languages"]);
308
+ yield* redis.sUnionStore("all_tags", ["tags", "languages"]);
309
+ yield* redis.sDiffStore("unique_tags", ["tags", "languages"]);
310
+
311
+ // Remove
312
+ yield* redis.sRem("tags", "python");
313
+
314
+ // Move
315
+ yield* redis.sMove("tags", "languages", "javascript");
316
+
317
+ // Scan
318
+ const scanResult = yield* redis.sScan("tags", 0);
319
+ });
320
+ ```
321
+
322
+ **Supported Set Commands:**
323
+ - `sAdd`, `sRem`, `sCard`, `sMembers`, `sPop`, `sRandMember`
324
+ - `sIsMember`, `smIsMember`, `sMove`
325
+ - `sInter`, `sInterCard`, `sInterStore`
326
+ - `sUnion`, `sUnionStore`
327
+ - `sDiff`, `sDiffStore`
328
+ - `sScan`
329
+
330
+ ### Sorted Set Commands
331
+
332
+ Sorted set commands operate on sets with scores that determine order.
333
+
334
+ ```typescript
335
+ const program = Effect.gen(function* () {
336
+ const redis = yield* RedisCore;
337
+
338
+ // Add members with scores
339
+ yield* redis.zAdd("leaderboard", [
340
+ { score: 100, member: "Alice" },
341
+ { score: 90, member: "Bob" },
342
+ { score: 85, member: "Charlie" },
343
+ ]);
344
+
345
+ // Get score
346
+ const score = yield* redis.zScore("leaderboard", "Alice");
347
+ const scores = yield* redis.zmScore("leaderboard", [
348
+ "Alice",
349
+ "Bob",
350
+ ]);
351
+
352
+ // Rank operations (0-indexed, ascending by default)
353
+ const rank = yield* redis.zRank("leaderboard", "Alice");
354
+ const revRank = yield* redis.zRevRank("leaderboard", "Alice");
355
+
356
+ // Count
357
+ const total = yield* redis.zCard("leaderboard");
358
+ const inRange = yield* redis.zCount("leaderboard", 80, 100);
359
+
360
+ // Range operations
361
+ const topThree = yield* redis.zRange("leaderboard", 0, 2);
362
+ const topThreeScores = yield* redis.zRange("leaderboard", 0, 2, {
363
+ withScores: true,
364
+ });
365
+
366
+ // Range by score
367
+ const highScorers = yield* redis.zRangeByScore("leaderboard", 90, 100);
368
+
369
+ // Range by lex
370
+ const lexRange = yield* redis.zRangeByLex("leaderboard", "-", "+");
371
+
372
+ // Pop operations
373
+ const highest = yield* redis.zPopMax("leaderboard");
374
+ const lowest = yield* redis.zPopMin("leaderboard");
375
+
376
+ // Increment score
377
+ yield* redis.zIncrBy("leaderboard", 10, "Bob");
378
+
379
+ // Remove
380
+ yield* redis.zRem("leaderboard", "Charlie");
381
+ yield* redis.zRemRangeByRank("leaderboard", 0, 1);
382
+ yield* redis.zRemRangeByScore("leaderboard", 0, 50);
383
+
384
+ // Set operations
385
+ const inter = yield* redis.zInter(["leaderboard", "active_players"]);
386
+ yield* redis.zInterStore("top_active", 2, ["leaderboard", "active_players"]);
387
+
388
+ const union = yield* redis.zUnion(["leaderboard", "archive"]);
389
+ yield* redis.zUnionStore("all_time", 2, ["leaderboard", "archive"]);
390
+
391
+ // Scan
392
+ const scanResult = yield* redis.zScan("leaderboard", 0);
393
+ });
394
+ ```
395
+
396
+ **Supported Sorted Set Commands:**
397
+ - `zAdd`, `zRem`, `zCard`, `zCount`, `zScore`, `zmScore`
398
+ - `zRank`, `zRevRank`, `zRange`, `zRevRange`
399
+ - `zRangeByScore`, `zRangeByLex`, `zRangeStore`
400
+ - `zPopMax`, `zPopMin`, `zMPop`
401
+ - `zIncrBy`, `zRandMember`
402
+ - `zRemRangeByRank`, `zRemRangeByScore`, `zRemRangeByLex`
403
+ - `zLexCount`, `zInter`, `zInterCard`, `zInterStore`
404
+ - `zUnion`, `zUnionStore`, `zDiff`, `zDiffStore`
405
+ - `zScan`
406
+
407
+ ### Generic Commands
408
+
409
+ Generic commands work with all data types.
410
+
411
+ ```typescript
412
+ const program = Effect.gen(function* () {
413
+ const redis = yield* RedisCore;
414
+
415
+ // Check existence
416
+ const exists = yield* redis.exists(["key1", "key2", "key3"]);
417
+
418
+ // Get type
419
+ const type = yield* redis.type("mykey");
420
+
421
+ // Delete keys
422
+ yield* redis.del(["key1", "key2"]);
423
+ yield* redis.unlink(["key3", "key4"]); // Async delete
424
+
425
+ // Expiration
426
+ yield* redis.expire("tempkey", 3600); // 1 hour in seconds
427
+ yield* redis.expireAt("tempkey", Math.floor(Date.now() / 1000) + 3600);
428
+ yield* redis.pExpire("tempkey", 3600000); // milliseconds
429
+ yield* redis.pExpireAt("tempkey", Date.now() + 3600000);
430
+
431
+ const ttl = yield* redis.ttl("tempkey"); // seconds
432
+ const pttl = yield* redis.pTTL("tempkey"); // milliseconds
433
+ yield* redis.persist("tempkey"); // Remove expiration
434
+
435
+ // Get expiration time
436
+ const expirationTime = yield* redis.expireTime("tempkey"); // Unix timestamp in seconds
437
+ const pExpirationTime = yield* redis.pExpireTime("tempkey"); // Unix timestamp in milliseconds
438
+
439
+ // Rename
440
+ yield* redis.rename("oldkey", "newkey");
441
+ yield* redis.renameNX("oldkey", "newkey"); // Only if newkey doesn't exist
442
+
443
+ // Copy
444
+ yield* redis.copy("source", "destination");
445
+
446
+ // Keys pattern matching
447
+ const allKeys = yield* redis.keys("*");
448
+ const userKeys = yield* redis.keys("user:*");
449
+
450
+ // Random key
451
+ const randomKey = yield* redis.randomKey();
452
+
453
+ // Scan (cursor-based iteration)
454
+ const scanResult = yield* redis.scan(0, { pattern: "user:*", count: 10 });
455
+
456
+ // Sort
457
+ const sorted = yield* redis.sort("mylist", {
458
+ by: "weight_*",
459
+ limit: { offset: 0, count: 10 },
460
+ get: ["object_*", "#"],
461
+ alpha: true,
462
+ direction: "DESC",
463
+ });
464
+
465
+ // Touch (update last access time)
466
+ const touched = yield* redis.touch(["key1", "key2"]);
467
+
468
+ // Dump and restore
469
+ const serialized = yield* redis.dump("mykey");
470
+ yield* redis.restore("newkey", 0, serialized);
471
+
472
+ // Migrate (move to another Redis instance)
473
+ yield* redis.migrate("target-host", 6379, "mykey", 0, 5000);
474
+
475
+ // Wait for replication
476
+ yield* redis.wait(1, 1000); // Wait for 1 replica within 1000ms
477
+ });
478
+ ```
479
+
480
+ **Supported Generic Commands:**
481
+ - `del`, `exists`, `expire`, `expireAt`, `expireTime`
482
+ - `keys`, `migrate`, `move`, `persist`
483
+ - `pExpire`, `pExpireAt`, `pExpireTime`, `pTTL`
484
+ - `randomKey`, `rename`, `renameNX`, `restore`
485
+ - `scan`, `sort`, `sortRo`, `touch`, `ttl`, `type`
486
+ - `unlink`, `wait`, `copy`, `dump`
487
+
488
+ ### JSON Commands
489
+
490
+ JSON commands store and manipulate JSON documents (requires Redis Stack).
491
+
492
+ ```typescript
493
+ const program = Effect.gen(function* () {
494
+ const redis = yield* RedisCore;
495
+
496
+ // Set JSON value
497
+ yield* redis.json.set("user:1", "$", {
498
+ name: "Alice",
499
+ age: 30,
500
+ tags: ["developer", "typescript"],
501
+ metadata: {
502
+ created: "2024-01-01",
503
+ active: true,
504
+ },
505
+ });
506
+
507
+ // Get JSON value
508
+ const user = yield* redis.json.get("user:1");
509
+ const name = yield* redis.json.get("user:1", { path: "$.name" });
510
+
511
+ // Update specific path
512
+ yield* redis.json.set("user:1", "$.age", 31);
513
+
514
+ // String operations
515
+ yield* redis.json.strAppend("user:1", "$.name", " Smith");
516
+ const nameLength = yield* redis.json.strLen("user:1", "$.name");
517
+
518
+ // Numeric operations
519
+ yield* redis.json.numIncrBy("user:1", "$.age", 1);
520
+ yield* redis.json.numMultBy("user:1", "$.age", 2);
521
+
522
+ // Array operations
523
+ yield* redis.json.arrAppend("user:1", "$.tags", "nodejs");
524
+ yield* redis.json.arrInsert("user:1", "$.tags", 0, "javascript");
525
+ const tagsLength = yield* redis.json.arrLen("user:1", "$.tags");
526
+ const tag = yield* redis.json.arrPop("user:1", "$.tags");
527
+ yield* redis.json.arrTrim("user:1", "$.tags", 0, 2);
528
+ const tagIndex = yield* redis.json.arrIndex(
529
+ "user:1",
530
+ "$.tags",
531
+ "typescript",
532
+ );
533
+
534
+ // Object operations
535
+ const objKeys = yield* redis.json.objKeys("user:1", "$.metadata");
536
+ const objLen = yield* redis.json.objLen("user:1", "$.metadata");
537
+
538
+ // Type checking
539
+ const valueType = yield* redis.json.type("user:1", "$");
540
+
541
+ // Boolean toggle
542
+ yield* redis.json.toggle("user:1", "$.metadata.active");
543
+
544
+ // Delete path
545
+ yield* redis.json.del("user:1", "$.metadata.created");
546
+
547
+ // Merge JSON
548
+ yield* redis.json.merge("user:1", "$", { lastLogin: "2024-01-15" });
549
+
550
+ // Multiple keys
551
+ const users = yield* redis.json.mGet(["user:1", "user:2", "user:3"], "$");
552
+
553
+ // Multiple set
554
+ yield* redis.json.mSet([
555
+ { key: "user:4", path: "$", value: { name: "Bob" } },
556
+ { key: "user:5", path: "$", value: { name: "Charlie" } },
557
+ ]);
558
+
559
+ // Clear (sets arrays to [] and objects to {})
560
+ yield* redis.json.clear("user:1");
561
+
562
+ // Debug memory (get memory usage)
563
+ const memoryUsage = yield* redis.json.debugMemory("user:1");
564
+ });
565
+ ```
566
+
567
+ **Supported JSON Commands:**
568
+ - `get`, `set`, `del`, `clear`, `merge`, `forget`
569
+ - `mGet`, `mSet`
570
+ - `type`, `debugMemory`
571
+ - `strAppend`, `strLen`
572
+ - `numIncrBy`, `numMultBy`
573
+ - `arrAppend`, `arrIndex`, `arrInsert`, `arrLen`, `arrPop`, `arrTrim`
574
+ - `objKeys`, `objLen`
575
+ - `toggle`
576
+
577
+ ### Geospatial Commands
578
+
579
+ Geospatial commands store and query geographic coordinates.
580
+
581
+ ```typescript
582
+ const program = Effect.gen(function* () {
583
+ const redis = yield* RedisCore;
584
+
585
+ // Add locations
586
+ yield* redis.geoAdd("cities", [
587
+ {
588
+ longitude: -122.419,
589
+ latitude: 37.775,
590
+ member: "San Francisco",
591
+ },
592
+ {
593
+ longitude: -118.243,
594
+ latitude: 34.052,
595
+ member: "Los Angeles",
596
+ },
597
+ { longitude: -87.629, latitude: 41.878, member: "Chicago" },
598
+ ]);
599
+
600
+ // Get positions
601
+ const positions = yield* redis.geoPos("cities", [
602
+ "San Francisco",
603
+ "Los Angeles",
604
+ ]);
605
+
606
+ // Get distance
607
+ const distance = yield* redis.geoDist(
608
+ "cities",
609
+ "San Francisco",
610
+ "Los Angeles",
611
+ "km",
612
+ );
613
+
614
+ // Get geohash
615
+ const hashes = yield* redis.geoHash("cities", [
616
+ "San Francisco",
617
+ "Los Angeles",
618
+ ]);
619
+
620
+ // Find nearby locations
621
+ const nearby = yield* redis.geoRadius(
622
+ "cities",
623
+ -122.419,
624
+ 37.775,
625
+ 100,
626
+ "km",
627
+ );
628
+
629
+ // Find nearby from member
630
+ const nearbyFromMember = yield* redis.geoRadiusByMember(
631
+ "cities",
632
+ "San Francisco",
633
+ 100,
634
+ "km",
635
+ );
636
+
637
+ // Search in box/circle (Redis 6.2+)
638
+ const searchBox = yield* redis.geoSearch("cities", {
639
+ member: "San Francisco",
640
+ byBox: { width: 200, height: 200, unit: "km" },
641
+ });
642
+
643
+ const searchCircle = yield* redis.geoSearch("cities", {
644
+ member: "San Francisco",
645
+ byRadius: { radius: 100, unit: "km" },
646
+ });
647
+
648
+ // Store search results
649
+ yield* redis.geoSearchStore("nearby", "cities", {
650
+ member: "San Francisco",
651
+ byRadius: { radius: 100, unit: "km" },
652
+ });
653
+ });
654
+ ```
655
+
656
+ **Supported Geospatial Commands:**
657
+ - `geoAdd`, `geoDist`, `geoHash`, `geoPos`
658
+ - `geoRadius`, `geoRadiusByMember`
659
+ - `geoRadiusRo`, `geoRadiusByMemberRo`
660
+ - `geoSearch`, `geoSearchStore`
661
+
662
+ ### Bitmap Commands
663
+
664
+ Bitmap commands perform bitwise operations on strings.
665
+
666
+ ```typescript
667
+ const program = Effect.gen(function* () {
668
+ const redis = yield* RedisCore;
669
+
670
+ // Set and get bits
671
+ yield* redis.setBit("bitmap", 7, 1);
672
+ const bit = yield* redis.getBit("bitmap", 7);
673
+
674
+ // Count set bits
675
+ const count = yield* redis.bitCount("bitmap");
676
+ const countRange = yield* redis.bitCount("bitmap", { start: 0, end: 10 });
677
+
678
+ // Find first set/clear bit
679
+ const firstSet = yield* redis.bitPos("bitmap", 1);
680
+ const firstClear = yield* redis.bitPos("bitmap", 0);
681
+ const firstSetAfter = yield* redis.bitPos("bitmap", 1, { start: 5 });
682
+
683
+ // Bitwise operations
684
+ yield* redis.bitOp("AND", "dest", ["bitmap1", "bitmap2"]);
685
+ yield* redis.bitOp("OR", "dest", ["bitmap1", "bitmap2"]);
686
+ yield* redis.bitOp("XOR", "dest", ["bitmap1", "bitmap2"]);
687
+ yield* redis.bitOp("NOT", "dest", ["bitmap1"]);
688
+
689
+ // Bitfield operations
690
+ const fieldResult = yield* redis.bitField("mykey", {
691
+ operations: [
692
+ { type: "u4", offset: 0, operation: "GET" },
693
+ { type: "u4", offset: 4, operation: "SET", value: 15 },
694
+ ],
695
+ });
696
+ });
697
+ ```
698
+
699
+ **Supported Bitmap Commands:**
700
+ - `getBit`, `setBit`, `bitCount`, `bitPos`
701
+ - `bitOp`, `bitField`, `bitFieldRo`
702
+
703
+ ### HyperLogLog Commands
704
+
705
+ HyperLogLog commands provide probabilistic cardinality estimation (count unique elements).
706
+
707
+ ```typescript
708
+ const program = Effect.gen(function* () {
709
+ const redis = yield* RedisCore;
710
+
711
+ // Add elements
712
+ yield* redis.pfAdd("hll", "a", "b", "c", "d", "e");
713
+ yield* redis.pfAdd("hll", "f", "g");
714
+
715
+ // Get cardinality (approximate count of unique elements)
716
+ const count = yield* redis.pfCount(["hll"]);
717
+
718
+ // Count across multiple HyperLogLogs
719
+ const multiCount = yield* redis.pfCount(["hll1", "hll2", "hll3"]);
720
+
721
+ // Merge HyperLogLogs
722
+ yield* redis.pfMerge("combined", ["hll1", "hll2", "hll3"]);
723
+
724
+ const mergedCount = yield* redis.pfCount(["combined"]);
725
+ });
726
+ ```
727
+
728
+ **Supported HyperLogLog Commands:**
729
+ - `pfAdd`, `pfCount`, `pfMerge`
730
+
731
+ ### Scripting Commands
732
+
733
+ Scripting commands allow server-side Lua script execution.
734
+
735
+ ```typescript
736
+ const program = Effect.gen(function* () {
737
+ const redis = yield* RedisCore;
738
+
739
+ // Execute Lua script
740
+ const result = yield* redis.eval(
741
+ "return redis.call('GET',KEYS[1])",
742
+ {
743
+ keys: ["mykey"],
744
+ arguments: [],
745
+ },
746
+ );
747
+
748
+ // Script with arguments
749
+ const sum = yield* redis.eval(
750
+ `
751
+ local val1 = redis.call('GET', KEYS[1])
752
+ local val2 = tonumber(ARGV[1])
753
+ return val1 + val2
754
+ `,
755
+ {
756
+ keys: ["counter"],
757
+ arguments: ["10"],
758
+ },
759
+ );
760
+
761
+ // Load script (returns SHA1)
762
+ const sha = yield* redis.scriptLoad("return 'Hello Redis'");
763
+
764
+ // Execute by SHA
765
+ const cachedResult = yield* redis.evalSha(sha, {
766
+ keys: [],
767
+ arguments: [],
768
+ });
769
+
770
+ // Check if scripts exist
771
+ const exists = yield* redis.scriptExists([sha]);
772
+
773
+ // Flush script cache
774
+ yield* redis.scriptFlush();
775
+
776
+ // Function operations (Redis 7.0+)
777
+ yield* redis.functionLoad("#!lua name=mylib\n return 'hello'");
778
+ const functions = yield* redis.functionList();
779
+ yield* redis.functionFlush();
780
+ });
781
+ ```
782
+
783
+ **Supported Scripting Commands:**
784
+ - `eval`, `evalSha`, `evalRo`, `evalShaRo`
785
+ - `scriptDebug`, `scriptExists`, `scriptFlush`, `scriptKill`, `scriptLoad`
786
+ - `fCall`, `fCallRo`
787
+ - `functionDelete`, `functionDump`, `functionFlush`, `functionKill`
788
+ - `functionList`, `functionLoad`, `functionRestore`, `functionStats`
789
+
790
+ ### Transactions (MULTI/EXEC)
791
+
792
+ Transactions allow you to batch multiple commands and execute them atomically.
793
+
794
+ ```typescript
795
+ const program = Effect.gen(function* () {
796
+ const redis = yield* RedisCore;
797
+
798
+ // Start a transaction
799
+ const [result, execResult] = yield* redis.multi((tx) =>
800
+ Effect.gen(function* () {
801
+ yield* tx.set("key1", "value1");
802
+ yield* tx.set("key2", "value2");
803
+ const val = yield* tx.get("key1");
804
+ yield* tx.incr("counter");
805
+
806
+ return val; // This is the "result" returned
807
+ }),
808
+ );
809
+
810
+ // result contains the return value from the transaction block
811
+ yield* Effect.log(`Transaction result: ${result}`);
812
+
813
+ // execResult is an array of responses from each command
814
+ // null if transaction was aborted
815
+ yield* Effect.log(`Exec responses: ${execResult}`);
816
+
817
+ // Transaction with condition
818
+ const [txResult, execOK] = yield* redis.multi((tx) =>
819
+ Effect.gen(function* () {
820
+ const currentValue = yield* tx.get("balance");
821
+
822
+ if (currentValue && Number(currentValue) >= 100) {
823
+ yield* tx.decrBy("balance", 100);
824
+ yield* tx.incrBy("savings", 100);
825
+ return true;
826
+ }
827
+
828
+ return false;
829
+ }),
830
+ );
831
+ });
832
+ ```
833
+
834
+ ### Pipelines
835
+
836
+ Pipelines batch multiple commands for more efficient execution (like transactions but without atomicity guarantees).
837
+
838
+ ```typescript
839
+ const program = Effect.gen(function* () {
840
+ const redis = yield* RedisCore;
841
+
842
+ // Execute multiple commands in a pipeline
843
+ const [result, execResult] = yield* redis.pipeline((pipe) =>
844
+ Effect.gen(function* () {
845
+ yield* pipe.set("key1", "value1");
846
+ yield* pipe.set("key2", "value2");
847
+ yield* pipe.set("key3", "value3");
848
+ const val = yield* pipe.get("key1");
849
+ yield* pipe.mGet(["key1", "key2", "key3"]);
850
+
851
+ return val;
852
+ }),
853
+ );
854
+
855
+ // result contains the return value from the pipeline block
856
+ yield* Effect.log(`Pipeline result: ${result}`);
857
+
858
+ // execResult is an array of responses from each command
859
+ yield* Effect.log(`Pipeline responses:`, execResult);
860
+
861
+ // Pipeline with many operations
862
+ const [bulkResult, responses] = yield* redis.pipeline((pipe) =>
863
+ Effect.gen(function* () {
864
+ // Simulate bulk data operations
865
+ for (let i = 0; i < 1000; i++) {
866
+ yield* pipe.set(`key:${i}`, `value:${i}`);
867
+ }
868
+ return "bulk operation complete";
869
+ }),
870
+ );
871
+ });
872
+ ```
873
+
874
+ ## Complete Command Reference
875
+
876
+ ### String Commands (24)
877
+
878
+ ```
879
+ append, decr, decrBy, delEx, digest, get, getDel, getEx, getRange
880
+ getSet, incr, incrBy, incrByFloat, lcs, mGet, mSet, mSetEx, mSetNX
881
+ pSetEx, set, setEx, setNX, setRange, strLen
882
+ ```
883
+
884
+ ### Hash Commands (25)
885
+
886
+ ```
887
+ hDel, hExists, hExpire, hExpireAt, hExpireTime, hGet, hGetAll, hGetDel
888
+ hGetEx, hIncrBy, hIncrByFloat, hKeys, hLen, hmGet, hPersist, hpExpire
889
+ hpExpireAt, hpExpireTime, hpTTL, hRandField, hScan, hSet, hSetEx
890
+ hSetNX, hStrLen, hTTL, hVals
891
+ ```
892
+
893
+ ### List Commands (21)
894
+
895
+ ```
896
+ blMove, blmPop, blPop, brPop, brPopLPush, lIndex, lInsert, lLen, lMove
897
+ lPop, lPos, lPush, lPushX, lRange, lRem, lSet, lTrim, rPop
898
+ rPopLPush, rPush, rPushX
899
+ ```
900
+
901
+ ### Set Commands (17)
902
+
903
+ ```
904
+ sAdd, sCard, sDiff, sDiffStore, sInter, sInterCard, sInterStore, sIsMember
905
+ sMembers, smIsMember, sMove, sPop, sRandMember, sRem, sScan, sUnion
906
+ sUnionStore
907
+ ```
908
+
909
+ ### Sorted Set Commands (34)
910
+
911
+ ```
912
+ bzMPop, bzPopMax, bzPopMin, zAdd, zCard, zCount, zDiff, zDiffStore
913
+ zIncrBy, zInter, zInterCard, zInterStore, zLexCount, zmPop, zmScore
914
+ zPopMax, zPopMin, zRandMember, zRange, zRangeByLex, zRangeByScore
915
+ zRangeStore, zRank, zRem, zRemRangeByLex, zRemRangeByRank
916
+ zRemRangeByScore, zRevRank, zScan, zScore, zUnion, zUnionStore
917
+ ```
918
+
919
+ ### Generic Commands (30)
920
+
921
+ ```
922
+ copy, del, dump, exists, expire, expireAt, expireTime, keys, migrate
923
+ move, persist, pExpire, pExpireAt, pExpireTime, pTTL, randomKey
924
+ rename, renameNX, restore, scan, sort, sortRo, touch, ttl, type
925
+ unlink, wait
926
+ ```
927
+
928
+ ### JSON Commands (24)
929
+
930
+ ```
931
+ arrAppend, arrIndex, arrInsert, arrLen, arrPop, arrTrim, clear, debugMemory
932
+ del, forget, get, merge, mGet, mSet, numIncrBy, numMultBy, objKeys
933
+ objLen, set, strAppend, strLen, toggle, type
934
+ ```
935
+
936
+ ### Geospatial Commands (10)
937
+
938
+ ```
939
+ geoAdd, geoDist, geoHash, geoPos, geoRadius, geoRadiusByMember
940
+ geoRadiusByMemberRo, geoRadiusRo, geoSearch, geoSearchStore
941
+ ```
942
+
943
+ ### Bitmap Commands (7)
944
+
945
+ ```
946
+ bitCount, bitField, bitFieldRo, bitOp, bitPos, getBit, setBit
947
+ ```
948
+
949
+ ### HyperLogLog Commands (3)
950
+
951
+ ```
952
+ pfAdd, pfCount, pfMerge
953
+ ```
954
+
955
+ ### Scripting Commands (19)
956
+
957
+ ```
958
+ eval, evalRo, evalSha, evalShaRo, fCall, fCallRo, functionDelete
959
+ functionDump, functionFlush, functionKill, functionList, functionLoad
960
+ functionRestore, functionStats, scriptDebug, scriptExists, scriptFlush
961
+ scriptKill, scriptLoad
962
+ ```
963
+
964
+ ## Error Handling
965
+
966
+ effect-redis uses Effect's error handling system with a custom `RedisError` type:
967
+
968
+ ```typescript
969
+ import { Effect } from "effect";
970
+ import { RedisError } from "@envoy1084/effect-redis";
971
+
972
+ const program = Effect.gen(function* () {
973
+ const redis = yield* RedisCore;
974
+
975
+ // Errors are automatically caught and wrapped in RedisError
976
+ const result = yield* redis.get("mykey").pipe(
977
+ Effect.catchTag("RedisError", (error) => {
978
+ yield* Effect.log(`Redis error: ${error.message}`);
979
+ return Effect.succeed("default_value");
980
+ }),
981
+ );
982
+ });
983
+ ```
984
+
985
+ ## Advanced Usage
986
+
987
+ ### Combining Commands with Effect
988
+
989
+ ```typescript
990
+ import { Effect, Array as EffectArray } from "effect";
991
+
992
+ const program = Effect.gen(function* () {
993
+ const redis = yield* RedisCore;
994
+
995
+ // Process multiple keys in parallel
996
+ const keys = ["user:1", "user:2", "user:3"];
997
+ const users = yield* Effect.all(
998
+ keys.map((key) => redis.get(key)),
999
+ { concurrency: 3 },
1000
+ );
1001
+
1002
+ // Conditional operations
1003
+ const exists = yield* redis.exists(["mykey"]);
1004
+ if (exists > 0) {
1005
+ const value = yield* redis.get("mykey");
1006
+ yield* Effect.log(value);
1007
+ }
1008
+
1009
+ // Error recovery
1010
+ const value = yield* redis
1011
+ .get("mayNotExist")
1012
+ .pipe(
1013
+ Effect.orElse(() => Effect.succeed("default")),
1014
+ );
1015
+ });
1016
+ ```
1017
+
1018
+ ### Custom Context for Dependency Injection
1019
+
1020
+ ```typescript
1021
+ import { Context, Effect, Layer } from "effect";
1022
+
1023
+ class MyService extends Context.Tag("MyService")<
1024
+ MyService,
1025
+ { doSomething: () => Effect.Effect<string> }
1026
+ >() {}
1027
+
1028
+ const myServiceLive = Layer.succeed(MyService, {
1029
+ doSomething: () => Effect.succeed("done"),
1030
+ });
1031
+
1032
+ const program = Effect.gen(function* () {
1033
+ const redis = yield* RedisCore;
1034
+ const service = yield* MyService;
1035
+
1036
+ const result = yield* service.doSomething();
1037
+ yield* redis.set("result", result);
1038
+ });
1039
+
1040
+ // Compose layers
1041
+ const AppLayer = Layer.mergeAll(RedisLayer, myServiceLive);
1042
+ program.pipe(Effect.provide(AppLayer), runMain);
1043
+ ```
1044
+ ## Future Work
1045
+
1046
+ Following command groups are supported:
1047
+
1048
+ - [x] String commands
1049
+ - [x] Hash commands
1050
+ - [x] List commands
1051
+ - [x] Set commands
1052
+ - [x] Sorted set commands
1053
+ - [ ] Stream commands
1054
+ - [x] Bitmap commands
1055
+ - [x] HyperLogLog commands
1056
+ - [x] Geospatial commands
1057
+ - [x] JSON commands
1058
+ - [ ] Search commands
1059
+ - [ ] Time series commands
1060
+ - [ ] Vector set commands
1061
+ - [ ] Pub/Sub commands
1062
+ - [x] Transaction commands (Pipeline and Multi)
1063
+ - [x] Scripting commands
1064
+ - [ ] Connection commands
1065
+ - [ ] Server commands
1066
+ - [ ] Cluster commands
1067
+ - [x] Generic commands
1068
+
1069
+ We are working on adding support for other Redis command groups. If you have any suggestions or requests, please feel free to open an issue or submit a pull request.
1070
+
1071
+ ## Contributing
1072
+
1073
+ Contributions are welcome! Please feel free to submit a Pull Request.
1074
+
1075
+ ## License
1076
+
1077
+ MIT