git.iko.soy/rowtype-yoga/purescript-yoga-redis
on main help sign in
Initial commit: Extract yoga-redis from purescript-yoga-backend-stack
commitb659fa16ead3e6476e0a214e830ee2580c67a4a7 authorMark <mark.eibes@gmail.com> dateWed 4 Feb 2026, 19:31 (8 months ago)
10 files changed+1414
.gitignore+24 LICENSE+21 README.md+187 package.json+27 spago.yaml+32 src/Yoga/Redis.js+162 src/Yoga/Redis.purs+387 src/Yoga/Redis/PubSub.js+55 src/Yoga/Redis/PubSub.purs+82 test/Main.purs+437
.gitignorenew file+24
@@ -0,0 +1,24 @@
1+# Dependencies and build outputs
2+.spago/
3+output/
4+output-*/
5+node_modules/
6+
7+# PureScript IDE
8+.psc-ide-port
9+.psci_modules/
10+
11+# Logs
12+*.log
13+
14+# OS files
15+.DS_Store
16+
17+# Editor and backup files
18+*.backup
19+*.bak
20+*.bak[0-9]
21+*.bak[0-9][0-9]
22+*~
23+*.swp
24+*.swo
LICENSEnew file+21
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 Rowtype Yoga
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.
README.mdnew file+187
@@ -0,0 +1,187 @@
1+# yoga-redis
2+
3+Type-safe PureScript FFI bindings for Redis using ioredis.
4+
5+## Overview
6+
7+This package provides comprehensive bindings to Redis operations including:
8+- String operations (get, set, setex, etc.)
9+- Hash operations (hget, hset, hgetall, etc.)
10+- List operations (lpush, rpush, lpop, rpop, lrange, etc.)
11+- Set operations (sadd, srem, smembers, etc.)
12+- Sorted set operations (zadd, zrem, zrange, etc.)
13+- Pub/Sub support
14+- Connection management
15+- TTL and expiry operations
16+
17+## Installation
18+
19+```bash
20+spago install yoga-redis
21+```
22+
23+Make sure to also install the ioredis npm package:
24+
25+```bash
26+npm install ioredis
27+```
28+
29+## Usage
30+
31+### Basic Operations
32+
33+```purescript
34+import Yoga.Redis as Redis
35+import Effect.Aff (launchAff_)
36+
37+main = launchAff_ do
38+ -- Create and connect
39+ client <- Redis.createClient { url: "redis://localhost:6379" }
40+ Redis.connect client
41+
42+ -- String operations
43+ Redis.set client (Redis.RedisKey "mykey") "myvalue"
44+ value <- Redis.get client (Redis.RedisKey "mykey")
45+
46+ -- With expiry
47+ Redis.setex client (Redis.RedisKey "tempkey") (Redis.RedisTTL 60) "tempvalue"
48+
49+ -- Hash operations
50+ Redis.hset client (Redis.RedisKey "user:1") "name" "John"
51+ Redis.hset client (Redis.RedisKey "user:1") "email" "john@example.com"
52+ user <- Redis.hgetall client (Redis.RedisKey "user:1")
53+
54+ -- List operations
55+ Redis.lpush client (Redis.RedisKey "mylist") ["item1", "item2"]
56+ items <- Redis.lrange client (Redis.RedisKey "mylist") 0 (-1)
57+
58+ -- Cleanup
59+ Redis.disconnect client
60+```
61+
62+### Configuration
63+
64+```purescript
65+import Yoga.Redis as Redis
66+
67+config :: Redis.RedisConfig
68+config =
69+ { host: Redis.RedisHost "localhost"
70+ , port: Redis.RedisPort 6379
71+ , password: Just (Redis.RedisPassword "mypassword")
72+ , db: Just (Redis.RedisDatabase 0)
73+ , keyPrefix: Just (Redis.RedisKeyPrefix "myapp:")
74+ , connectTimeout: Just (Redis.ConnectTimeout (Milliseconds 10000.0))
75+ }
76+
77+main = launchAff_ do
78+ client <- Redis.createClientWithConfig config
79+ -- ... use client
80+```
81+
82+### Pub/Sub
83+
84+```purescript
85+import Yoga.Redis.PubSub as RedisPubSub
86+
87+main = launchAff_ do
88+ -- Create separate clients for pub and sub
89+ pubClient <- Redis.createClient { url: "redis://localhost:6379" }
90+ subClient <- Redis.createClient { url: "redis://localhost:6379" }
91+
92+ -- Subscribe to channel
93+ RedisPubSub.subscribe subClient "notifications" \message -> do
94+ log $ "Received: " <> message
95+
96+ -- Publish message
97+ RedisPubSub.publish pubClient "notifications" "Hello, World!"
98+```
99+
100+## Type Safety
101+
102+All Redis keys are wrapped in a `RedisKey` newtype for type safety:
103+
104+```purescript
105+newtype RedisKey = RedisKey String
106+```
107+
108+Similarly, TTL values, passwords, and other configuration values use newtypes:
109+
110+```purescript
111+newtype RedisTTL = RedisTTL Int
112+newtype RedisPassword = RedisPassword String
113+newtype RedisHost = RedisHost String
114+newtype RedisPort = RedisPort Int
115+```
116+
117+## API Reference
118+
119+### Connection Management
120+
121+- `createClient` - Create Redis client from URL
122+- `createClientWithConfig` - Create client with detailed configuration
123+- `connect` - Connect to Redis
124+- `disconnect` - Disconnect from Redis
125+- `quit` - Gracefully quit connection
126+- `ping` - Test connection
127+
128+### String Operations
129+
130+- `get` - Get value by key
131+- `set` - Set key to value
132+- `setex` - Set key with expiry time
133+- `del` - Delete key(s)
134+- `exists` - Check if key exists
135+- `incr` / `incrBy` - Increment value
136+- `decr` / `decrBy` - Decrement value
137+
138+### Hash Operations
139+
140+- `hget` - Get field from hash
141+- `hset` - Set field in hash
142+- `hgetall` - Get all fields from hash
143+- `hdel` - Delete field from hash
144+- `hexists` - Check if field exists
145+- `hkeys` - Get all field names
146+- `hlen` - Get number of fields
147+
148+### List Operations
149+
150+- `lpush` / `rpush` - Push to list
151+- `lpop` / `rpop` - Pop from list
152+- `lrange` - Get range of elements
153+- `llen` - Get list length
154+
155+### Set Operations
156+
157+- `sadd` - Add members to set
158+- `srem` - Remove members from set
159+- `smembers` - Get all members
160+- `sismember` - Check membership
161+- `scard` - Get set size
162+
163+### Sorted Set Operations
164+
165+- `zadd` - Add members with scores
166+- `zrem` - Remove members
167+- `zrange` - Get range by index
168+- `zcard` - Get sorted set size
169+- `zscore` - Get member score
170+
171+### TTL Operations
172+
173+- `expire` - Set key expiry
174+- `ttl` - Get time to live
175+
176+## Integration with Om
177+
178+For Om-based applications, see [yoga-redis-om](../yoga-redis-om) which provides Om-wrapped operations and layer management.
179+
180+## Related Packages
181+
182+- [yoga-redis-om](../yoga-redis-om) - Om-wrapped Redis operations
183+- [ioredis](https://github.com/luin/ioredis) - The underlying Node.js Redis client
184+
185+## License
186+
187+MIT
package.jsonnew file+27
@@ -0,0 +1,27 @@
1+{
2+ "name": "purescript-yoga-redis",
3+ "version": "0.1.0",
4+ "description": "PureScript FFI bindings for Redis using ioredis",
5+ "license": "MIT",
6+ "repository": {
7+ "type": "git",
8+ "url": "https://github.com/rowtype-yoga/purescript-yoga-backend-stack.git",
9+ "directory": "packages/yoga-redis"
10+ },
11+ "keywords": [
12+ "purescript",
13+ "redis",
14+ "ioredis",
15+ "ffi",
16+ "bindings",
17+ "yoga"
18+ ],
19+ "scripts": {
20+ "test": "spago test",
21+ "test:bash": "./test-with-docker.sh",
22+ "test:ts": "bun run test-with-docker.ts"
23+ },
24+ "dependencies": {
25+ "ioredis": "^5.3.0"
26+ }
27+}
spago.yamlnew file+32
@@ -0,0 +1,32 @@
1+package:
2+ name: yoga-redis
3+ publish:
4+ version: 0.1.0
5+ license: MIT
6+ location:
7+ githubOwner: rowtype-yoga
8+ githubRepo: purescript-yoga-backend-stack
9+ subdir: packages/yoga-redis
10+ dependencies:
11+ - aff: ">=7.0.0 <8.0.0"
12+ - arrays: ">=7.0.0 <8.0.0"
13+ - datetime: ">=6.0.0 <7.0.0"
14+ - effect: ">=4.0.0 <5.0.0"
15+ - either: ">=6.0.0 <7.0.0"
16+ - foldable-traversable: ">=6.0.0 <7.0.0"
17+ - foreign-object: ">=4.0.0 <5.0.0"
18+ - js-promise: ">=1.0.0 <2.0.0"
19+ - js-promise-aff: ">=1.0.0 <2.0.0"
20+ - maybe: ">=6.0.0 <7.0.0"
21+ - newtype: ">=5.0.0 <6.0.0"
22+ - nullable: ">=6.0.0 <7.0.0"
23+ - prelude: ">=6.0.0 <7.0.0"
24+ - record: ">=4.0.0 <5.0.0"
25+ - transformers: ">=6.0.0 <7.0.0"
26+ test:
27+ main: Test.Redis.Main
28+ dependencies:
29+ - spec: ">=7.0.0 <8.0.0"
30+ - exceptions: ">=6.0.0 <7.0.0"
31+ - console: ">=6.0.0 <7.0.0"
32+ - yoga-test-docker
src/Yoga/Redis.jsnew file+162
@@ -0,0 +1,162 @@
1+import Redis from 'ioredis';
2+
3+// Create Redis client
4+export const createRedisImpl = (config) => new Redis(config);
5+
6+// Connection
7+export const connectImpl = (redis) => redis.connect();
8+
9+export const disconnectImpl = (redis) => Promise.resolve(redis.disconnect());
10+
11+export const quitImpl = (redis) => redis.quit();
12+
13+export const pingImpl = (redis) => redis.ping();
14+
15+// String Operations
16+export const getImpl = (redis, key) => redis.get(key);
17+
18+export const setImpl = (redis, key, value, opts) => {
19+ const args = [key, value];
20+
21+ if (opts.ex !== undefined) {
22+ args.push('EX', opts.ex);
23+ }
24+ if (opts.px !== undefined) {
25+ args.push('PX', opts.px);
26+ }
27+ if (opts.nx === true) {
28+ args.push('NX');
29+ }
30+ if (opts.xx === true) {
31+ args.push('XX');
32+ }
33+ if (opts.keepttl === true) {
34+ args.push('KEEPTTL');
35+ }
36+
37+ return redis.set(...args);
38+};
39+
40+export const setexImpl = (redis, key, seconds, value) =>
41+ redis.setex(key, seconds, value);
42+
43+export const delImpl = (redis, keys) => redis.del(...keys);
44+
45+export const existsImpl = (redis, keys) => redis.exists(...keys);
46+
47+export const expireImpl = (redis, key, seconds) =>
48+ redis.expire(key, seconds).then(result => result === 1);
49+
50+export const ttlImpl = (redis, key) => redis.ttl(key);
51+
52+export const incrImpl = (redis, key) => redis.incr(key);
53+
54+export const incrByImpl = (redis, key, increment) => redis.incrby(key, increment);
55+
56+export const decrImpl = (redis, key) => redis.decr(key);
57+
58+export const decrByImpl = (redis, key, decrement) => redis.decrby(key, decrement);
59+
60+// Hash Operations
61+export const hgetImpl = (redis, key, field) => redis.hget(key, field);
62+
63+export const hsetImpl = (redis, key, fieldValues) => {
64+ const args = [key];
65+ for (const fv of fieldValues) {
66+ args.push(fv.field, fv.value);
67+ }
68+ return redis.hset(...args);
69+};
70+
71+export const hgetallImpl = async (redis, key) => {
72+ const obj = await redis.hgetall(key);
73+ return Object.entries(obj).map(([field, value]) => ({ field, value }));
74+};
75+
76+export const hdelImpl = (redis, key, fields) => redis.hdel(key, ...fields);
77+
78+export const hexistsImpl = (redis, key, field) =>
79+ redis.hexists(key, field).then(result => result === 1);
80+
81+export const hkeysImpl = (redis, key) => redis.hkeys(key);
82+
83+export const hlenImpl = (redis, key) => redis.hlen(key);
84+
85+// List Operations
86+export const lpushImpl = (redis, key, values) => redis.lpush(key, ...values);
87+
88+export const rpushImpl = (redis, key, values) => redis.rpush(key, ...values);
89+
90+export const lpopImpl = (redis, key) => redis.lpop(key);
91+
92+export const rpopImpl = (redis, key) => redis.rpop(key);
93+
94+export const lrangeImpl = (redis, key, start, stop) => redis.lrange(key, start, stop);
95+
96+export const llenImpl = (redis, key) => redis.llen(key);
97+
98+// Set Operations
99+export const saddImpl = (redis, key, members) => redis.sadd(key, ...members);
100+
101+export const sremImpl = (redis, key, members) => redis.srem(key, ...members);
102+
103+export const smembersImpl = (redis, key) => redis.smembers(key);
104+
105+export const sismemberImpl = (redis, key, member) =>
106+ redis.sismember(key, member).then(result => result === 1);
107+
108+export const scardImpl = (redis, key) => redis.scard(key);
109+
110+// Sorted Set Operations
111+export const zaddImpl = (redis, key, members, opts) => {
112+ const args = [key];
113+
114+ if (opts.nx === true) {
115+ args.push('NX');
116+ }
117+ if (opts.xx === true) {
118+ args.push('XX');
119+ }
120+ if (opts.gt === true) {
121+ args.push('GT');
122+ }
123+ if (opts.lt === true) {
124+ args.push('LT');
125+ }
126+ if (opts.ch === true) {
127+ args.push('CH');
128+ }
129+
130+ for (const member of members) {
131+ args.push(member.score, member.value);
132+ }
133+
134+ return redis.zadd(...args);
135+};
136+
137+export const zremImpl = (redis, key, members) => redis.zrem(key, ...members);
138+
139+export const zrangeImpl = async (redis, key, start, stop) => {
140+ const results = await redis.zrange(key, start, stop, 'WITHSCORES');
141+ const members = [];
142+
143+ // Results come as [value1, score1, value2, score2, ...]
144+ for (let i = 0; i < results.length; i += 2) {
145+ members.push({
146+ value: results[i],
147+ score: parseFloat(results[i + 1])
148+ });
149+ }
150+
151+ return members;
152+};
153+
154+export const zcardImpl = (redis, key) => redis.zcard(key);
155+
156+export const zscoreImpl = async (redis, key, member) => {
157+ const score = await redis.zscore(key, member);
158+ return score !== null ? parseFloat(score) : null;
159+};
160+
161+// Pub/Sub Operations
162+export const publishImpl = (redis, channel, message) => redis.publish(channel, message);
src/Yoga/Redis.pursnew file+387
@@ -0,0 +1,387 @@
1+module Yoga.Redis where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe)
6+import Data.Newtype (class Newtype)
7+import Data.Nullable (Nullable)
8+import Data.Nullable as Nullable
9+import Data.Time.Duration (Milliseconds)
10+import Effect (Effect)
11+import Effect.Aff (Aff)
12+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4)
13+import Prim.Row (class Union)
14+import Promise (Promise)
15+import Promise.Aff (toAffE) as Promise
16+
17+-- Opaque Redis client type
18+foreign import data Redis :: Type
19+
20+-- Newtypes for type safety
21+
22+-- Connection configuration
23+newtype RedisHost = RedisHost String
24+derive instance Newtype RedisHost _
25+derive newtype instance Eq RedisHost
26+derive newtype instance Show RedisHost
27+
28+newtype RedisPort = RedisPort Int
29+derive instance Newtype RedisPort _
30+derive newtype instance Eq RedisPort
31+derive newtype instance Show RedisPort
32+
33+newtype RedisPassword = RedisPassword String
34+derive instance Newtype RedisPassword _
35+derive newtype instance Eq RedisPassword
36+derive newtype instance Show RedisPassword
37+
38+newtype RedisDatabase = RedisDatabase Int
39+derive instance Newtype RedisDatabase _
40+derive newtype instance Eq RedisDatabase
41+derive newtype instance Show RedisDatabase
42+
43+newtype RedisKeyPrefix = RedisKeyPrefix String
44+derive instance Newtype RedisKeyPrefix _
45+derive newtype instance Eq RedisKeyPrefix
46+derive newtype instance Show RedisKeyPrefix
47+
48+newtype ConnectTimeout = ConnectTimeout Milliseconds
49+derive instance Newtype ConnectTimeout _
50+derive newtype instance Eq ConnectTimeout
51+derive newtype instance Ord ConnectTimeout
52+derive newtype instance Show ConnectTimeout
53+
54+newtype KeepAlive = KeepAlive Milliseconds
55+derive instance Newtype KeepAlive _
56+derive newtype instance Eq KeepAlive
57+derive newtype instance Ord KeepAlive
58+derive newtype instance Show KeepAlive
59+
60+newtype IPFamily = IPFamily Int
61+derive instance Newtype IPFamily _
62+derive newtype instance Eq IPFamily
63+derive newtype instance Show IPFamily
64+
65+newtype RetryDelay = RetryDelay Milliseconds
66+derive instance Newtype RetryDelay _
67+derive newtype instance Eq RetryDelay
68+derive newtype instance Ord RetryDelay
69+derive newtype instance Show RetryDelay
70+
71+-- Data types
72+newtype RedisKey = RedisKey String
73+derive instance Newtype RedisKey _
74+derive newtype instance Eq RedisKey
75+derive newtype instance Ord RedisKey
76+derive newtype instance Show RedisKey
77+
78+newtype RedisValue = RedisValue String
79+derive instance Newtype RedisValue _
80+derive newtype instance Eq RedisValue
81+derive newtype instance Show RedisValue
82+
83+newtype RedisField = RedisField String
84+derive instance Newtype RedisField _
85+derive newtype instance Eq RedisField
86+derive newtype instance Ord RedisField
87+derive newtype instance Show RedisField
88+
89+newtype RedisChannel = RedisChannel String
90+derive instance Newtype RedisChannel _
91+derive newtype instance Eq RedisChannel
92+derive newtype instance Show RedisChannel
93+
94+newtype RedisPattern = RedisPattern String
95+derive instance Newtype RedisPattern _
96+derive newtype instance Eq RedisPattern
97+derive newtype instance Show RedisPattern
98+
99+newtype TTLSeconds = TTLSeconds Int
100+derive instance Newtype TTLSeconds _
101+derive newtype instance Eq TTLSeconds
102+derive newtype instance Ord TTLSeconds
103+derive newtype instance Show TTLSeconds
104+
105+newtype TTLMilliseconds = TTLMilliseconds Milliseconds
106+derive instance Newtype TTLMilliseconds _
107+derive newtype instance Eq TTLMilliseconds
108+derive newtype instance Ord TTLMilliseconds
109+derive newtype instance Show TTLMilliseconds
110+
111+newtype RedisScore = RedisScore Number
112+derive instance Newtype RedisScore _
113+derive newtype instance Eq RedisScore
114+derive newtype instance Ord RedisScore
115+derive newtype instance Show RedisScore
116+
117+-- Create Redis client
118+type RedisConfigImpl =
119+ ( host :: RedisHost
120+ , port :: RedisPort
121+ , password :: RedisPassword
122+ , db :: RedisDatabase
123+ , keyPrefix :: RedisKeyPrefix
124+ , connectTimeout :: ConnectTimeout
125+ , keepAlive :: KeepAlive
126+ , family :: IPFamily
127+ , retryStrategy :: Effect (Maybe RetryDelay)
128+ )
129+
130+foreign import createRedisImpl :: forall opts. EffectFn1 { | opts } Redis
131+
132+createRedis :: forall opts opts_. Union opts opts_ RedisConfigImpl => { | opts } -> Effect Redis
133+createRedis opts = runEffectFn1 createRedisImpl opts
134+
135+-- Connect
136+foreign import connectImpl :: EffectFn1 Redis (Promise Unit)
137+
138+connect :: Redis -> Aff Unit
139+connect = runEffectFn1 connectImpl >>> Promise.toAffE
140+
141+-- Disconnect
142+foreign import disconnectImpl :: EffectFn1 Redis (Promise Unit)
143+
144+disconnect :: Redis -> Aff Unit
145+disconnect = runEffectFn1 disconnectImpl >>> Promise.toAffE
146+
147+-- Quit (graceful disconnect)
148+foreign import quitImpl :: EffectFn1 Redis (Promise Unit)
149+
150+quit :: Redis -> Aff Unit
151+quit = runEffectFn1 quitImpl >>> Promise.toAffE
152+
153+-- Ping
154+foreign import pingImpl :: EffectFn1 Redis (Promise String)
155+
156+ping :: Redis -> Aff String
157+ping = runEffectFn1 pingImpl >>> Promise.toAffE
158+
159+-- String Operations
160+
161+-- Get
162+foreign import getImpl :: EffectFn2 Redis RedisKey (Promise (Nullable RedisValue))
163+
164+get :: RedisKey -> Redis -> Aff (Maybe RedisValue)
165+get key redis = runEffectFn2 getImpl redis key # Promise.toAffE <#> Nullable.toMaybe
166+
167+-- Set
168+type SetOptionsImpl = (ex :: TTLSeconds, px :: TTLMilliseconds, nx :: Boolean, xx :: Boolean, keepttl :: Boolean)
169+
170+foreign import setImpl :: forall opts. EffectFn4 Redis RedisKey RedisValue { | opts } (Promise Unit)
171+
172+set :: forall opts opts_. Union opts opts_ SetOptionsImpl => RedisKey -> RedisValue -> { | opts } -> Redis -> Aff Unit
173+set key value opts redis = runEffectFn4 setImpl redis key value opts # Promise.toAffE
174+
175+-- SetEx (set with expiry in seconds)
176+foreign import setexImpl :: EffectFn4 Redis RedisKey TTLSeconds RedisValue (Promise Unit)
177+
178+setex :: RedisKey -> TTLSeconds -> RedisValue -> Redis -> Aff Unit
179+setex key ttl value redis = runEffectFn4 setexImpl redis key ttl value # Promise.toAffE
180+
181+-- Del
182+foreign import delImpl :: EffectFn2 Redis (Array RedisKey) (Promise Int)
183+
184+del :: Array RedisKey -> Redis -> Aff Int
185+del keys redis = runEffectFn2 delImpl redis keys # Promise.toAffE
186+
187+-- Exists
188+foreign import existsImpl :: EffectFn2 Redis (Array RedisKey) (Promise Int)
189+
190+exists :: Array RedisKey -> Redis -> Aff Int
191+exists keys redis = runEffectFn2 existsImpl redis keys # Promise.toAffE
192+
193+-- Expire
194+foreign import expireImpl :: EffectFn3 Redis RedisKey TTLSeconds (Promise Boolean)
195+
196+expire :: RedisKey -> TTLSeconds -> Redis -> Aff Boolean
197+expire key ttl redis = runEffectFn3 expireImpl redis key ttl # Promise.toAffE
198+
199+-- TTL
200+foreign import ttlImpl :: EffectFn2 Redis RedisKey (Promise Int)
201+
202+ttl :: RedisKey -> Redis -> Aff Int
203+ttl key redis = runEffectFn2 ttlImpl redis key # Promise.toAffE
204+
205+-- Increment
206+foreign import incrImpl :: EffectFn2 Redis RedisKey (Promise Int)
207+
208+incr :: RedisKey -> Redis -> Aff Int
209+incr key redis = runEffectFn2 incrImpl redis key # Promise.toAffE
210+
211+-- IncrBy
212+foreign import incrByImpl :: EffectFn3 Redis RedisKey Int (Promise Int)
213+
214+incrBy :: RedisKey -> Int -> Redis -> Aff Int
215+incrBy key increment redis = runEffectFn3 incrByImpl redis key increment # Promise.toAffE
216+
217+-- Decrement
218+foreign import decrImpl :: EffectFn2 Redis RedisKey (Promise Int)
219+
220+decr :: RedisKey -> Redis -> Aff Int
221+decr key redis = runEffectFn2 decrImpl redis key # Promise.toAffE
222+
223+-- DecrBy
224+foreign import decrByImpl :: EffectFn3 Redis RedisKey Int (Promise Int)
225+
226+decrBy :: RedisKey -> Int -> Redis -> Aff Int
227+decrBy key decrement redis = runEffectFn3 decrByImpl redis key decrement # Promise.toAffE
228+
229+-- Hash Operations
230+
231+-- HGet
232+foreign import hgetImpl :: EffectFn3 Redis RedisKey RedisField (Promise (Nullable RedisValue))
233+
234+hget :: RedisKey -> RedisField -> Redis -> Aff (Maybe RedisValue)
235+hget key field redis = runEffectFn3 hgetImpl redis key field # Promise.toAffE <#> Nullable.toMaybe
236+
237+-- HSet
238+foreign import hsetImpl :: EffectFn3 Redis RedisKey (Array { field :: RedisField, value :: RedisValue }) (Promise Int)
239+
240+hset :: RedisKey -> Array { field :: RedisField, value :: RedisValue } -> Redis -> Aff Int
241+hset key fieldValues redis = runEffectFn3 hsetImpl redis key fieldValues # Promise.toAffE
242+
243+-- HGetAll
244+foreign import hgetallImpl :: EffectFn2 Redis RedisKey (Promise (Array { field :: RedisField, value :: RedisValue }))
245+
246+hgetall :: RedisKey -> Redis -> Aff (Array { field :: RedisField, value :: RedisValue })
247+hgetall key redis = runEffectFn2 hgetallImpl redis key # Promise.toAffE
248+
249+-- HDel
250+foreign import hdelImpl :: EffectFn3 Redis RedisKey (Array RedisField) (Promise Int)
251+
252+hdel :: RedisKey -> Array RedisField -> Redis -> Aff Int
253+hdel key fields redis = runEffectFn3 hdelImpl redis key fields # Promise.toAffE
254+
255+-- HExists
256+foreign import hexistsImpl :: EffectFn3 Redis RedisKey RedisField (Promise Boolean)
257+
258+hexists :: RedisKey -> RedisField -> Redis -> Aff Boolean
259+hexists key field redis = runEffectFn3 hexistsImpl redis key field # Promise.toAffE
260+
261+-- HKeys
262+foreign import hkeysImpl :: EffectFn2 Redis RedisKey (Promise (Array RedisField))
263+
264+hkeys :: RedisKey -> Redis -> Aff (Array RedisField)
265+hkeys key redis = runEffectFn2 hkeysImpl redis key # Promise.toAffE
266+
267+-- HLen
268+foreign import hlenImpl :: EffectFn2 Redis RedisKey (Promise Int)
269+
270+hlen :: RedisKey -> Redis -> Aff Int
271+hlen key redis = runEffectFn2 hlenImpl redis key # Promise.toAffE
272+
273+-- List Operations
274+
275+-- LPush
276+foreign import lpushImpl :: EffectFn3 Redis RedisKey (Array RedisValue) (Promise Int)
277+
278+lpush :: RedisKey -> Array RedisValue -> Redis -> Aff Int
279+lpush key values redis = runEffectFn3 lpushImpl redis key values # Promise.toAffE
280+
281+-- RPush
282+foreign import rpushImpl :: EffectFn3 Redis RedisKey (Array RedisValue) (Promise Int)
283+
284+rpush :: RedisKey -> Array RedisValue -> Redis -> Aff Int
285+rpush key values redis = runEffectFn3 rpushImpl redis key values # Promise.toAffE
286+
287+-- LPop
288+foreign import lpopImpl :: EffectFn2 Redis RedisKey (Promise (Nullable RedisValue))
289+
290+lpop :: RedisKey -> Redis -> Aff (Maybe RedisValue)
291+lpop key redis = runEffectFn2 lpopImpl redis key # Promise.toAffE <#> Nullable.toMaybe
292+
293+-- RPop
294+foreign import rpopImpl :: EffectFn2 Redis RedisKey (Promise (Nullable RedisValue))
295+
296+rpop :: RedisKey -> Redis -> Aff (Maybe RedisValue)
297+rpop key redis = runEffectFn2 rpopImpl redis key # Promise.toAffE <#> Nullable.toMaybe
298+
299+-- LRange
300+foreign import lrangeImpl :: EffectFn4 Redis RedisKey Int Int (Promise (Array RedisValue))
301+
302+lrange :: RedisKey -> Int -> Int -> Redis -> Aff (Array RedisValue)
303+lrange key start stop redis = runEffectFn4 lrangeImpl redis key start stop # Promise.toAffE
304+
305+-- LLen
306+foreign import llenImpl :: EffectFn2 Redis RedisKey (Promise Int)
307+
308+llen :: RedisKey -> Redis -> Aff Int
309+llen key redis = runEffectFn2 llenImpl redis key # Promise.toAffE
310+
311+-- Set Operations
312+
313+-- SAdd
314+foreign import saddImpl :: EffectFn3 Redis RedisKey (Array RedisValue) (Promise Int)
315+
316+sadd :: RedisKey -> Array RedisValue -> Redis -> Aff Int
317+sadd key members redis = runEffectFn3 saddImpl redis key members # Promise.toAffE
318+
319+-- SRem
320+foreign import sremImpl :: EffectFn3 Redis RedisKey (Array RedisValue) (Promise Int)
321+
322+srem :: RedisKey -> Array RedisValue -> Redis -> Aff Int
323+srem key members redis = runEffectFn3 sremImpl redis key members # Promise.toAffE
324+
325+-- SMembers
326+foreign import smembersImpl :: EffectFn2 Redis RedisKey (Promise (Array RedisValue))
327+
328+smembers :: RedisKey -> Redis -> Aff (Array RedisValue)
329+smembers key redis = runEffectFn2 smembersImpl redis key # Promise.toAffE
330+
331+-- SIsMember
332+foreign import sismemberImpl :: EffectFn3 Redis RedisKey RedisValue (Promise Boolean)
333+
334+sismember :: RedisKey -> RedisValue -> Redis -> Aff Boolean
335+sismember key member redis = runEffectFn3 sismemberImpl redis key member # Promise.toAffE
336+
337+-- SCard
338+foreign import scardImpl :: EffectFn2 Redis RedisKey (Promise Int)
339+
340+scard :: RedisKey -> Redis -> Aff Int
341+scard key redis = runEffectFn2 scardImpl redis key # Promise.toAffE
342+
343+-- Sorted Set Operations
344+
345+-- ZAdd
346+type ZAddMember = { score :: RedisScore, value :: RedisValue }
347+
348+type ZAddOptionsImpl = (nx :: Boolean, xx :: Boolean, gt :: Boolean, lt :: Boolean, ch :: Boolean)
349+
350+foreign import zaddImpl :: forall opts. EffectFn4 Redis RedisKey (Array ZAddMember) { | opts } (Promise Int)
351+
352+zadd :: forall opts opts_. Union opts opts_ ZAddOptionsImpl => RedisKey -> Array ZAddMember -> { | opts } -> Redis -> Aff Int
353+zadd key members opts redis = runEffectFn4 zaddImpl redis key members opts # Promise.toAffE
354+
355+-- ZRem
356+foreign import zremImpl :: EffectFn3 Redis RedisKey (Array RedisValue) (Promise Int)
357+
358+zrem :: RedisKey -> Array RedisValue -> Redis -> Aff Int
359+zrem key members redis = runEffectFn3 zremImpl redis key members # Promise.toAffE
360+
361+-- ZRange
362+type ZRangeMember = { value :: RedisValue, score :: RedisScore }
363+
364+foreign import zrangeImpl :: EffectFn4 Redis RedisKey Int Int (Promise (Array ZRangeMember))
365+
366+zrange :: RedisKey -> Int -> Int -> Redis -> Aff (Array ZRangeMember)
367+zrange key start stop redis = runEffectFn4 zrangeImpl redis key start stop # Promise.toAffE
368+
369+-- ZCard
370+foreign import zcardImpl :: EffectFn2 Redis RedisKey (Promise Int)
371+
372+zcard :: RedisKey -> Redis -> Aff Int
373+zcard key redis = runEffectFn2 zcardImpl redis key # Promise.toAffE
374+
375+-- ZScore
376+foreign import zscoreImpl :: EffectFn3 Redis RedisKey RedisValue (Promise (Nullable RedisScore))
377+
378+zscore :: RedisKey -> RedisValue -> Redis -> Aff (Maybe RedisScore)
379+zscore key member redis = runEffectFn3 zscoreImpl redis key member # Promise.toAffE <#> Nullable.toMaybe
380+
381+-- Pub/Sub Operations
382+
383+-- Publish
384+foreign import publishImpl :: EffectFn3 Redis RedisChannel RedisValue (Promise Int)
385+
386+publish :: RedisChannel -> RedisValue -> Redis -> Aff Int
387+publish channel message redis = runEffectFn3 publishImpl redis channel message # Promise.toAffE
src/Yoga/Redis/PubSub.jsnew file+55
@@ -0,0 +1,55 @@
1+import Redis from 'ioredis';
2+
3+// Create subscriber (separate Redis instance for subscriptions)
4+export const createSubscriberImpl = (config) => new Redis(config);
5+
6+// Subscribe to channels
7+export const subscribeImpl = (subscriber, channels, handler) => {
8+ // Set up message handler
9+ subscriber.on('message', (channel, message) => {
10+ handler({
11+ channel,
12+ message
13+ })();
14+ });
15+
16+ // Subscribe to channels
17+ return subscriber.subscribe(...channels);
18+};
19+
20+// Unsubscribe from channels
21+export const unsubscribeImpl = (subscriber, channels) => {
22+ if (channels.length === 0) {
23+ return subscriber.unsubscribe();
24+ }
25+ return subscriber.unsubscribe(...channels);
26+};
27+
28+// Pattern subscribe
29+export const psubscribeImpl = (subscriber, patterns, handler) => {
30+ // Set up pattern message handler
31+ subscriber.on('pmessage', (pattern, channel, message) => {
32+ handler({
33+ pattern,
34+ channel,
35+ message
36+ })();
37+ });
38+
39+ // Subscribe to patterns
40+ return subscriber.psubscribe(...patterns);
41+};
42+
43+// Pattern unsubscribe
44+export const punsubscribeImpl = (subscriber, patterns) => {
45+ if (patterns.length === 0) {
46+ return subscriber.punsubscribe();
47+ }
48+ return subscriber.punsubscribe(...patterns);
49+};
50+
51+// Disconnect subscriber
52+export const disconnectSubscriberImpl = (subscriber) => subscriber.disconnect();
53+
54+// Quit subscriber (graceful disconnect)
55+export const quitSubscriberImpl = (subscriber) => subscriber.quit();
src/Yoga/Redis/PubSub.pursnew file+82
@@ -0,0 +1,82 @@
1+module Yoga.Redis.PubSub where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Aff (Aff)
7+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, runEffectFn1, runEffectFn2, runEffectFn3)
8+import Prim.Row (class Union)
9+import Promise (Promise)
10+import Promise.Aff (toAffE) as Promise
11+import Yoga.Redis (ConnectTimeout, IPFamily, KeepAlive, Redis, RedisChannel, RedisDatabase, RedisHost, RedisKeyPrefix, RedisPassword, RedisPattern, RedisPort, RedisValue)
12+
13+-- Opaque subscriber type (separate from main Redis client for pub/sub)
14+foreign import data RedisSubscriber :: Type
15+
16+-- Subscription message payload
17+type SubscriptionMessage =
18+ { channel :: RedisChannel
19+ , message :: RedisValue
20+ }
21+
22+-- Pattern subscription message payload
23+type PatternMessage =
24+ { pattern :: RedisPattern
25+ , channel :: RedisChannel
26+ , message :: RedisValue
27+ }
28+
29+-- Create subscriber (separate client for subscriptions)
30+type RedisSubscriberConfigImpl =
31+ ( host :: RedisHost
32+ , port :: RedisPort
33+ , password :: RedisPassword
34+ , db :: RedisDatabase
35+ , keyPrefix :: RedisKeyPrefix
36+ , connectTimeout :: ConnectTimeout
37+ , keepAlive :: KeepAlive
38+ , family :: IPFamily
39+ )
40+
41+foreign import createSubscriberImpl :: forall opts. EffectFn1 { | opts } RedisSubscriber
42+
43+createSubscriber :: forall opts opts_. Union opts opts_ RedisSubscriberConfigImpl => { | opts } -> Effect RedisSubscriber
44+createSubscriber = runEffectFn1 createSubscriberImpl
45+
46+-- Subscribe to channels
47+foreign import subscribeImpl :: EffectFn3 RedisSubscriber (Array RedisChannel) (SubscriptionMessage -> Effect Unit) (Promise Unit)
48+
49+subscribe :: Array RedisChannel -> (SubscriptionMessage -> Effect Unit) -> RedisSubscriber -> Aff Unit
50+subscribe channels handler subscriber =
51+ runEffectFn3 subscribeImpl subscriber channels handler # Promise.toAffE
52+
53+-- Unsubscribe from channels
54+foreign import unsubscribeImpl :: EffectFn2 RedisSubscriber (Array RedisChannel) (Promise Unit)
55+
56+unsubscribe :: Array RedisChannel -> RedisSubscriber -> Aff Unit
57+unsubscribe channels subscriber = runEffectFn2 unsubscribeImpl subscriber channels # Promise.toAffE
58+
59+-- Pattern subscribe
60+foreign import psubscribeImpl :: EffectFn3 RedisSubscriber (Array RedisPattern) (PatternMessage -> Effect Unit) (Promise Unit)
61+
62+psubscribe :: Array RedisPattern -> (PatternMessage -> Effect Unit) -> RedisSubscriber -> Aff Unit
63+psubscribe patterns handler subscriber =
64+ runEffectFn3 psubscribeImpl subscriber patterns handler # Promise.toAffE
65+
66+-- Pattern unsubscribe
67+foreign import punsubscribeImpl :: EffectFn2 RedisSubscriber (Array RedisPattern) (Promise Unit)
68+
69+punsubscribe :: Array RedisPattern -> RedisSubscriber -> Aff Unit
70+punsubscribe patterns subscriber = runEffectFn2 punsubscribeImpl subscriber patterns # Promise.toAffE
71+
72+-- Disconnect subscriber
73+foreign import disconnectSubscriberImpl :: EffectFn1 RedisSubscriber (Promise Unit)
74+
75+disconnectSubscriber :: RedisSubscriber -> Aff Unit
76+disconnectSubscriber = runEffectFn1 disconnectSubscriberImpl >>> Promise.toAffE
77+
78+-- Quit subscriber (graceful disconnect)
79+foreign import quitSubscriberImpl :: EffectFn1 RedisSubscriber (Promise Unit)
80+
81+quitSubscriber :: RedisSubscriber -> Aff Unit
82+quitSubscriber = runEffectFn1 quitSubscriberImpl >>> Promise.toAffE
test/Main.pursnew file+437
@@ -0,0 +1,437 @@
1+module Test.Redis.Main where
2+
3+import Prelude
4+
5+import Data.Array (length)
6+import Data.Either (Either(..))
7+import Data.Maybe (Maybe(..), isJust)
8+import Data.Time.Duration (Milliseconds(..))
9+import Effect (Effect)
10+import Effect.Aff (Aff, delay, launchAff_, try, bracket)
11+import Effect.Class (liftEffect)
12+import Effect.Console (log)
13+import Test.Spec (Spec, around, describe, it)
14+import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
15+import Test.Spec.Reporter.Console (consoleReporter)
16+import Test.Spec.Runner (runSpec)
17+import Yoga.Test.Docker as Docker
18+import Yoga.Redis as Redis
19+
20+-- Test configuration
21+testHost :: Redis.RedisHost
22+testHost = Redis.RedisHost "localhost"
23+
24+testPort :: Redis.RedisPort
25+testPort = Redis.RedisPort 6380 -- Test port from docker-compose.test.yml
26+
27+-- Helper to create and manage Redis connection
28+withRedis :: (Redis.Redis -> Aff Unit) -> Aff Unit
29+withRedis test = do
30+ redis <- liftEffect $ Redis.createRedis { host: testHost, port: testPort }
31+ _ <- Redis.connect redis
32+ -- Clean up test keys before each test
33+ _ <- try $ Redis.del [ Redis.RedisKey "test:*" ] redis
34+ test redis
35+ _ <- Redis.quit redis
36+ pure unit
37+
38+-- Helper to check if value is Left
39+isLeft :: forall a b. Either a b -> Boolean
40+isLeft (Left _) = true
41+isLeft _ = false
42+
43+spec :: Spec Unit
44+spec = do
45+ describe "Yoga.Redis Integration Tests" do
46+
47+ -- Connection Management Tests
48+ around withRedis do
49+ describe "Connection Management" do
50+ it "connects to Redis successfully" \redis -> do
51+ pong <- Redis.ping redis
52+ pong `shouldEqual` "PONG"
53+
54+ it "handles ping operations" \redis -> do
55+ pong <- Redis.ping redis
56+ pong `shouldSatisfy` (_ == "PONG")
57+
58+ describe "Connection Errors" do
59+ it "handles connection failures gracefully" do
60+ result <- try do
61+ redis <- liftEffect $ Redis.createRedis
62+ { host: Redis.RedisHost "invalid-host-that-does-not-exist"
63+ , port: Redis.RedisPort 9999
64+ }
65+ _ <- Redis.connect redis
66+ Redis.ping redis
67+ result `shouldSatisfy` isLeft
68+
69+ -- String Operations Tests
70+ around withRedis do
71+ describe "String Operations" do
72+ it "sets and gets values" \redis -> do
73+ let key = Redis.RedisKey "test:string:key1"
74+ let value = Redis.RedisValue "test-value"
75+ _ <- Redis.set key value {} redis
76+ result <- Redis.get key redis
77+ result `shouldEqual` Just value
78+
79+ it "returns Nothing for non-existent keys" \redis -> do
80+ result <- Redis.get (Redis.RedisKey "test:nonexistent:key") redis
81+ result `shouldEqual` Nothing
82+
83+ it "deletes keys successfully" \redis -> do
84+ let key = Redis.RedisKey "test:string:delete"
85+ let value = Redis.RedisValue "value-to-delete"
86+ _ <- Redis.set key value {} redis
87+ count <- Redis.del [ key ] redis
88+ count `shouldEqual` 1
89+ result <- Redis.get key redis
90+ result `shouldEqual` Nothing
91+
92+ it "checks key existence" \redis -> do
93+ let key = Redis.RedisKey "test:string:exists"
94+ let value = Redis.RedisValue "exists-value"
95+ _ <- Redis.set key value {} redis
96+ existsCount <- Redis.exists [ key ] redis
97+ existsCount `shouldEqual` 1
98+
99+ _ <- Redis.del [ key ] redis
100+ notExistsCount <- Redis.exists [ key ] redis
101+ notExistsCount `shouldEqual` 0
102+
103+ it "increments and decrements counters" \redis -> do
104+ let key = Redis.RedisKey "test:string:counter"
105+
106+ -- Increment from 0 to 1
107+ val1 <- Redis.incr key redis
108+ val1 `shouldEqual` 1
109+
110+ -- Increment by 5
111+ val2 <- Redis.incrBy key 5 redis
112+ val2 `shouldEqual` 6
113+
114+ -- Decrement by 1
115+ val3 <- Redis.decr key redis
116+ val3 `shouldEqual` 5
117+
118+ -- Decrement by 3
119+ val4 <- Redis.decrBy key 3 redis
120+ val4 `shouldEqual` 2
121+
122+ -- Hash Operations Tests
123+ around withRedis do
124+ describe "Hash Operations" do
125+ it "sets and gets hash fields" \redis -> do
126+ let hashKey = Redis.RedisKey "test:hash:user1"
127+ let field1 = Redis.RedisField "name"
128+ let value1 = Redis.RedisValue "Alice"
129+
130+ _ <- Redis.hset hashKey [ { field: field1, value: value1 } ] redis
131+ result <- Redis.hget hashKey field1 redis
132+ result `shouldEqual` Just value1
133+
134+ it "gets all hash fields" \redis -> do
135+ let hashKey = Redis.RedisKey "test:hash:user2"
136+ let
137+ fields =
138+ [ { field: Redis.RedisField "name", value: Redis.RedisValue "Bob" }
139+ , { field: Redis.RedisField "email", value: Redis.RedisValue "bob@example.com" }
140+ , { field: Redis.RedisField "age", value: Redis.RedisValue "30" }
141+ ]
142+
143+ _ <- Redis.hset hashKey fields redis
144+ result <- Redis.hgetall hashKey redis
145+ length result `shouldEqual` 3
146+
147+ it "checks if hash field exists" \redis -> do
148+ let hashKey = Redis.RedisKey "test:hash:exists"
149+ let field = Redis.RedisField "field1"
150+ let value = Redis.RedisValue "value1"
151+
152+ _ <- Redis.hset hashKey [ { field, value } ] redis
153+ exists <- Redis.hexists hashKey field redis
154+ exists `shouldEqual` true
155+
156+ notExists <- Redis.hexists hashKey (Redis.RedisField "nonexistent") redis
157+ notExists `shouldEqual` false
158+
159+ it "deletes hash fields" \redis -> do
160+ let hashKey = Redis.RedisKey "test:hash:delete"
161+ let field = Redis.RedisField "temp"
162+ let value = Redis.RedisValue "temporary"
163+
164+ _ <- Redis.hset hashKey [ { field, value } ] redis
165+ count <- Redis.hdel hashKey [ field ] redis
166+ count `shouldEqual` 1
167+
168+ result <- Redis.hget hashKey field redis
169+ result `shouldEqual` Nothing
170+
171+ it "gets hash length" \redis -> do
172+ let hashKey = Redis.RedisKey "test:hash:len"
173+ let
174+ fields =
175+ [ { field: Redis.RedisField "f1", value: Redis.RedisValue "v1" }
176+ , { field: Redis.RedisField "f2", value: Redis.RedisValue "v2" }
177+ , { field: Redis.RedisField "f3", value: Redis.RedisValue "v3" }
178+ ]
179+
180+ _ <- Redis.hset hashKey fields redis
181+ len <- Redis.hlen hashKey redis
182+ len `shouldEqual` 3
183+
184+ -- List Operations Tests
185+ around withRedis do
186+ describe "List Operations" do
187+ it "pushes and pops from lists (right push, left pop)" \redis -> do
188+ let key = Redis.RedisKey "test:list:queue"
189+ let
190+ values =
191+ [ Redis.RedisValue "first"
192+ , Redis.RedisValue "second"
193+ , Redis.RedisValue "third"
194+ ]
195+
196+ len <- Redis.rpush key values redis
197+ len `shouldEqual` 3
198+
199+ popped <- Redis.lpop key redis
200+ popped `shouldEqual` Just (Redis.RedisValue "first")
201+
202+ it "pushes to left and pops from right" \redis -> do
203+ let key = Redis.RedisKey "test:list:stack"
204+ let value1 = Redis.RedisValue "bottom"
205+ let value2 = Redis.RedisValue "top"
206+
207+ _ <- Redis.lpush key [ value1 ] redis
208+ _ <- Redis.lpush key [ value2 ] redis
209+
210+ popped <- Redis.rpop key redis
211+ popped `shouldEqual` Just value1
212+
213+ it "gets list range" \redis -> do
214+ let key = Redis.RedisKey "test:list:range"
215+ let
216+ values =
217+ [ Redis.RedisValue "a"
218+ , Redis.RedisValue "b"
219+ , Redis.RedisValue "c"
220+ , Redis.RedisValue "d"
221+ ]
222+
223+ _ <- Redis.rpush key values redis
224+ result <- Redis.lrange key 0 (-1) redis
225+ result `shouldEqual` values
226+
227+ partial <- Redis.lrange key 1 2 redis
228+ length partial `shouldEqual` 2
229+
230+ it "gets list length" \redis -> do
231+ let key = Redis.RedisKey "test:list:length"
232+ let values = [ Redis.RedisValue "1", Redis.RedisValue "2", Redis.RedisValue "3" ]
233+
234+ _ <- Redis.rpush key values redis
235+ len <- Redis.llen key redis
236+ len `shouldEqual` 3
237+
238+ -- Set Operations Tests
239+ around withRedis do
240+ describe "Set Operations" do
241+ it "adds and checks members" \redis -> do
242+ let key = Redis.RedisKey "test:set:members"
243+ let member1 = Redis.RedisValue "member1"
244+ let member2 = Redis.RedisValue "member2"
245+
246+ count <- Redis.sadd key [ member1, member2 ] redis
247+ count `shouldEqual` 2
248+
249+ isMember <- Redis.sismember key member1 redis
250+ isMember `shouldEqual` true
251+
252+ isNotMember <- Redis.sismember key (Redis.RedisValue "nonexistent") redis
253+ isNotMember `shouldEqual` false
254+
255+ it "gets all set members" \redis -> do
256+ let key = Redis.RedisKey "test:set:all"
257+ let
258+ members =
259+ [ Redis.RedisValue "apple"
260+ , Redis.RedisValue "banana"
261+ , Redis.RedisValue "cherry"
262+ ]
263+
264+ _ <- Redis.sadd key members redis
265+ result <- Redis.smembers key redis
266+ length result `shouldEqual` 3
267+
268+ it "removes set members" \redis -> do
269+ let key = Redis.RedisKey "test:set:remove"
270+ let member = Redis.RedisValue "to-remove"
271+
272+ _ <- Redis.sadd key [ member ] redis
273+ count <- Redis.srem key [ member ] redis
274+ count `shouldEqual` 1
275+
276+ isMember <- Redis.sismember key member redis
277+ isMember `shouldEqual` false
278+
279+ it "gets set cardinality" \redis -> do
280+ let key = Redis.RedisKey "test:set:card"
281+ let
282+ members =
283+ [ Redis.RedisValue "1"
284+ , Redis.RedisValue "2"
285+ , Redis.RedisValue "3"
286+ , Redis.RedisValue "4"
287+ ]
288+
289+ _ <- Redis.sadd key members redis
290+ card <- Redis.scard key redis
291+ card `shouldEqual` 4
292+
293+ -- TTL and Expiration Tests
294+ around withRedis do
295+ describe "TTL and Expiration" do
296+ it "sets TTL on keys with setex" \redis -> do
297+ let key = Redis.RedisKey "test:ttl:setex"
298+ let value = Redis.RedisValue "expires-soon"
299+ let ttl = Redis.TTLSeconds 5
300+
301+ _ <- Redis.setex key ttl value redis
302+ ttlResult <- Redis.ttl key redis
303+ ttlResult `shouldSatisfy` (\t -> t > 0 && t <= 5)
304+
305+ it "sets expiry with expire command" \redis -> do
306+ let key = Redis.RedisKey "test:ttl:expire"
307+ let value = Redis.RedisValue "will-expire"
308+
309+ _ <- Redis.set key value {} redis
310+ success <- Redis.expire key (Redis.TTLSeconds 10) redis
311+ success `shouldEqual` true
312+
313+ ttlResult <- Redis.ttl key redis
314+ ttlResult `shouldSatisfy` (\t -> t > 0 && t <= 10)
315+
316+ it "expires keys after TTL (short wait test)" \redis -> do
317+ let key = Redis.RedisKey "test:ttl:wait"
318+ let value = Redis.RedisValue "expires-fast"
319+
320+ _ <- Redis.setex key (Redis.TTLSeconds 1) value redis
321+
322+ -- Check it exists initially
323+ initial <- Redis.get key redis
324+ initial `shouldSatisfy` isJust
325+
326+ -- Wait for expiration
327+ delay (Milliseconds 1200.0)
328+
329+ result <- Redis.get key redis
330+ result `shouldEqual` Nothing
331+
332+ it "returns -2 TTL for non-existent keys" \redis -> do
333+ let key = Redis.RedisKey "test:ttl:nonexistent"
334+ ttlResult <- Redis.ttl key redis
335+ ttlResult `shouldEqual` (-2)
336+
337+ it "returns -1 TTL for keys without expiration" \redis -> do
338+ let key = Redis.RedisKey "test:ttl:persistent"
339+ let value = Redis.RedisValue "no-expiry"
340+
341+ _ <- Redis.set key value {} redis
342+ ttlResult <- Redis.ttl key redis
343+ ttlResult `shouldEqual` (-1)
344+
345+ -- Sorted Set Operations Tests
346+ around withRedis do
347+ describe "Sorted Set Operations" do
348+ it "adds members with scores" \redis -> do
349+ let key = Redis.RedisKey "test:zset:leaderboard"
350+ let
351+ members =
352+ [ { score: Redis.RedisScore 100.0, value: Redis.RedisValue "player1" }
353+ , { score: Redis.RedisScore 200.0, value: Redis.RedisValue "player2" }
354+ , { score: Redis.RedisScore 150.0, value: Redis.RedisValue "player3" }
355+ ]
356+
357+ count <- Redis.zadd key members {} redis
358+ count `shouldEqual` 3
359+
360+ it "gets sorted set range with scores" \redis -> do
361+ let key = Redis.RedisKey "test:zset:range"
362+ let
363+ members =
364+ [ { score: Redis.RedisScore 1.0, value: Redis.RedisValue "a" }
365+ , { score: Redis.RedisScore 2.0, value: Redis.RedisValue "b" }
366+ , { score: Redis.RedisScore 3.0, value: Redis.RedisValue "c" }
367+ ]
368+
369+ _ <- Redis.zadd key members {} redis
370+ result <- Redis.zrange key 0 (-1) redis
371+ length result `shouldEqual` 3
372+
373+ -- Verify order (should be sorted by score)
374+ case result of
375+ [ first, _, _ ] -> first.value `shouldEqual` Redis.RedisValue "a"
376+ _ -> pure unit
377+
378+ it "gets member score" \redis -> do
379+ let key = Redis.RedisKey "test:zset:score"
380+ let member = { score: Redis.RedisScore 42.5, value: Redis.RedisValue "test" }
381+
382+ _ <- Redis.zadd key [ member ] {} redis
383+ score <- Redis.zscore key member.value redis
384+ score `shouldEqual` Just (Redis.RedisScore 42.5)
385+
386+ it "removes sorted set members" \redis -> do
387+ let key = Redis.RedisKey "test:zset:remove"
388+ let member = { score: Redis.RedisScore 10.0, value: Redis.RedisValue "remove-me" }
389+
390+ _ <- Redis.zadd key [ member ] {} redis
391+ count <- Redis.zrem key [ member.value ] redis
392+ count `shouldEqual` 1
393+
394+ score <- Redis.zscore key member.value redis
395+ score `shouldEqual` Nothing
396+
397+ it "gets sorted set cardinality" \redis -> do
398+ let key = Redis.RedisKey "test:zset:card"
399+ let
400+ members =
401+ [ { score: Redis.RedisScore 1.0, value: Redis.RedisValue "x" }
402+ , { score: Redis.RedisScore 2.0, value: Redis.RedisValue "y" }
403+ ]
404+
405+ _ <- Redis.zadd key members {} redis
406+ card <- Redis.zcard key redis
407+ card `shouldEqual` 2
408+
409+ -- Pub/Sub Tests
410+ around withRedis do
411+ describe "Pub/Sub Operations" do
412+ it "publishes messages to channel" \redis -> do
413+ let channel = Redis.RedisChannel "test:pubsub:channel"
414+ let message = Redis.RedisValue "test-message"
415+
416+ -- Publishing to a channel with no subscribers returns 0
417+ subscribers <- Redis.publish channel message redis
418+ subscribers `shouldSatisfy` (\s -> s >= 0)
419+
420+main :: Effect Unit
421+main = launchAff_ do
422+ liftEffect $ log "\n🧪 Starting Redis Integration Tests (with Docker)\n"
423+
424+ bracket
425+ -- Start Docker before tests
426+ ( do
427+ liftEffect $ log "⏳ Starting Redis and waiting for it to be ready..."
428+ Docker.startService "packages/yoga-redis/docker-compose.test.yml" 30
429+ liftEffect $ log "✅ Redis is ready!\n"
430+ )
431+ -- Stop Docker after tests (always runs!)
432+ ( \_ -> do
433+ Docker.stopService "packages/yoga-redis/docker-compose.test.yml"
434+ liftEffect $ log "✅ Cleanup complete\n"
435+ )
436+ -- Run tests
437+ (\_ -> runSpec [ consoleReporter ] spec)