effect-redis 0.0.31 → 0.0.32

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.
Files changed (3) hide show
  1. package/README.md +420 -120
  2. package/dist/index.js +1 -1
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -1,12 +1,33 @@
1
1
  # Effect-Redis
2
2
 
3
- A modern, type-safe [Effect](https://github.com/Effect-TS/effect) wrapper for the official `redis` client.
3
+ A modern, type-safe [Effect-TS](https://github.com/Effect-TS/effect) wrapper for
4
+ the official [`redis`](https://github.com/redis/node-redis) client.
4
5
 
5
- - **Resource-safe**: Automatic connection management via `Scope`.
6
- - **Modular Services**: Separate services for Core, Pub/Sub, and Streams.
7
- - **Shared Connection**: Share a single Redis connection across multiple services.
8
- - **Comprehensive API**: Pre-wrapped common commands with proper Effect error handling.
9
- - **Raw Access**: Escalate to the raw `node-redis` client when needed.
6
+ | Feature | Description |
7
+ | :--- | :--- |
8
+ | **Resource-safe** | Automatic connection lifecycle via `Scope` and `Layer.scoped` |
9
+ | **Modular services** | Independent services for Core, Pub/Sub, and Streams |
10
+ | **Shared connection** | Multiple services reuse a single managed connection |
11
+ | **Typed errors** | Tagged error union (`RedisConnectionError`, `RedisCommandError`, `RedisGeneralError`) |
12
+ | **Raw escape hatch** | Direct access to the underlying `node-redis` client via `use()` |
13
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Architecture](#architecture)
21
+ - [Services & Layers](#services--layers)
22
+ - [Configuration](#configuration)
23
+ - [Redis Service (Core)](#redis-service-core)
24
+ - [RedisPubSub Service](#redispubsub-service)
25
+ - [RedisStream Service](#redisstream-service)
26
+ - [Error Model](#error-model)
27
+ - [Types Reference](#types-reference)
28
+ - [Configuration Utilities](#configuration-utilities)
29
+ - [Usage Examples](#usage-examples)
30
+ - [Peer Dependencies](#peer-dependencies)
10
31
 
11
32
  ---
12
33
 
@@ -21,41 +42,37 @@ pnpm add effect-redis
21
42
  ## Quick Start
22
43
 
23
44
  ```ts
24
- import { Effect, Layer, Stream } from "effect";
45
+ import { Effect, Layer, Stream } from 'effect';
25
46
  import {
26
47
  RedisConnectionOptionsLive,
27
48
  RedisLive,
28
49
  RedisPubSubLive,
29
50
  Redis,
30
51
  RedisPubSub,
31
- } from "effect-redis";
52
+ } from 'effect-redis';
32
53
 
33
54
  const program = Effect.gen(function* () {
34
55
  const redis = yield* Redis;
35
56
  const pubsub = yield* RedisPubSub;
36
57
 
37
- // Key-Value operations
38
- yield* redis.set("user:42", JSON.stringify({ name: "Ada" }));
39
- const val = yield* redis.get("user:42");
58
+ yield* redis.set('user:42', JSON.stringify({ name: 'Ada' }));
59
+ const val = yield* redis.get('user:42');
40
60
 
41
- // Core commands
42
- yield* redis.set("counter", "1");
43
- yield* redis.incr("counter");
61
+ yield* redis.set('counter', '1');
62
+ yield* redis.incr('counter');
44
63
 
45
- // Pub/Sub
46
- const subscription = yield* pubsub.subscribe("notifications");
64
+ const subscription = yield* pubsub.subscribe('notifications');
47
65
  yield* Effect.fork(
48
66
  Stream.runForEach(subscription, (msg) =>
49
- Effect.log(`🔔 Received: ${msg}`)
67
+ Effect.log(`Received: ${msg}`)
50
68
  )
51
69
  );
52
70
 
53
- yield* pubsub.publish("notifications", "Hello world!");
71
+ yield* pubsub.publish('notifications', 'Hello world!');
54
72
  });
55
73
 
56
- // Compose the layers
57
74
  const RedisLayer = RedisConnectionOptionsLive({
58
- url: "redis://localhost:6379",
75
+ url: 'redis://localhost:6379',
59
76
  }).pipe(
60
77
  Layer.provideMerge(RedisLive),
61
78
  Layer.provideMerge(RedisPubSubLive)
@@ -66,92 +83,372 @@ Effect.runPromise(program.pipe(Effect.provide(RedisLayer)));
66
83
 
67
84
  ---
68
85
 
69
- ## Provided Services & Layers
86
+ ## Architecture
87
+
88
+ ```
89
+ RedisConnectionOptionsLive(options)
90
+ |
91
+ v
92
+ RedisConnectionLive (Layer.scoped — manages connection lifecycle)
93
+ / | \
94
+ v v v
95
+ RedisLive RedisPubSubLive RedisStreamLive
96
+ (core) (pub/sub) (streams)
97
+ ```
70
98
 
71
- The library is organized into several services, all of which can share a single underlying connection.
99
+ All service layers attempt to reuse a shared `RedisConnection` from context. If
100
+ none is provided, each creates its own scoped connection. Services that require
101
+ dedicated connections (Pub/Sub subscriber, Stream consumer) always create
102
+ additional clients internally.
72
103
 
73
- | Service | Layer | Purpose |
74
- | :--- | :--- | :--- |
75
- | `Redis` | `RedisLive` | Comprehensive Redis commands (Get, Set, Hash, List, etc.) |
76
- | `RedisPubSub` | `RedisPubSubLive` | Specialized Pub/Sub operations |
77
- | `RedisStream` | `RedisStreamLive` | Redis Streams operations (XADD, XREAD, XRANGE, etc.) |
78
- | `RedisConnection` | `RedisConnectionLive` | The underlying shared connection manager |
104
+ ---
79
105
 
80
- ### Connection Management
106
+ ## Services & Layers
81
107
 
82
- Use `RedisConnectionOptionsLive(options)` to provide connection settings. All service layers automatically use the shared `RedisConnection` if provided, otherwise they create their own managed connection.
108
+ | Service | Layer | Tag | Purpose |
109
+ | :--- | :--- | :--- | :--- |
110
+ | `Redis` | `RedisLive` | `'Redis'` | Comprehensive key-value, hash, list, set, sorted set, and admin commands |
111
+ | `RedisPubSub` | `RedisPubSubLive` | `'RedisPubSub'` | Publish/Subscribe messaging |
112
+ | `RedisStream` | `RedisStreamLive` | `'RedisStream'` | Redis Streams (XADD, XREAD, XRANGE, continuous polling) |
113
+ | `RedisConnection` | `RedisConnectionLive` | `'RedisConnection'` | Shared managed connection |
114
+ | `RedisConnectionOptions` | `RedisConnectionOptionsLive(opts)` | `'RedisConnectionOptions'` | Connection configuration provider |
115
+
116
+ ### Layer Composition
83
117
 
84
118
  ```ts
85
- const layers = RedisConnectionOptionsLive({ url: "..." }).pipe(
119
+ import { Layer } from 'effect';
120
+ import {
121
+ RedisConnectionOptionsLive,
122
+ RedisLive,
123
+ RedisPubSubLive,
124
+ RedisStreamLive,
125
+ } from 'effect-redis';
126
+
127
+ const FullRedisLayer = RedisConnectionOptionsLive({
128
+ url: 'redis://localhost:6379',
129
+ }).pipe(
86
130
  Layer.provideMerge(RedisLive),
87
- Layer.provideMerge(RedisPubSubLive)
131
+ Layer.provideMerge(RedisPubSubLive),
132
+ Layer.provideMerge(RedisStreamLive)
88
133
  );
89
134
  ```
90
135
 
91
136
  ---
92
137
 
93
- ## Reference
94
-
95
- ### `Redis` Service
96
- The most comprehensive service, wrapping hundreds of Redis commands.
97
- - `get(key)` / `set(key, value, options?)`
98
- - `del(...keys)` / `exists(...keys)`
99
- - `expire(key, seconds)` / `ttl(key)`
100
- - **Hashes**: `hset`, `hget`, `hgetall`, `hdel`, `hexists`, `hkeys`, `hvals`, `hlen`
101
- - **Lists**: `lpush`, `rpush`, `lpop`, `rpop`, `lrange`, `llen`, `lrem`
102
- - **Sets**: `sadd`, `srem`, `sismember`, `smembers`, `scard`
103
- - **Sorted Sets**: `zadd`, `zrange`, `zrangebyscore`, `zscore`, `zrem`, `zcard`
104
- - **Transactions**: `multi(commands)`
105
- - **Raw Execution**: `execute(command, ...args)`
106
- - **Raw Client Access**: `use(client => ...)`
107
-
108
- ### `RedisPubSub` Service
109
- - `publish(channel, message)`
110
- - `subscribe(channel)` -> Returns `Effect<Stream<string>>`
111
-
112
- ### `RedisStream` Service
113
- - `xadd(key, id, message)`
114
- - `xread(key, id, options?)`
115
- - `xrange(key, start, end, options?)`
116
- - `subscribe(key, options?)` -> Returns `Stream<StreamEntry>` (continuous polling)
117
- - `xack(key, group, ...ids)`
138
+ ## Configuration
139
+
140
+ `RedisConnectionOptionsLive` accepts the same options as `redis.createClient()`.
141
+ An optional second argument overrides the client factory (useful for testing).
142
+
143
+ ```ts
144
+ // URL-based
145
+ RedisConnectionOptionsLive({ url: 'redis://localhost:6379' });
146
+
147
+ // Host/port-based
148
+ RedisConnectionOptionsLive({
149
+ socket: { host: '10.0.0.1', port: 6380 },
150
+ password: 'secret',
151
+ database: 2,
152
+ });
153
+
154
+ // Custom client factory (e.g. for mocking)
155
+ RedisConnectionOptionsLive(
156
+ { url: 'redis://localhost:6379' },
157
+ (opts) => myMockClient(opts)
158
+ );
159
+ ```
160
+
161
+ ### Configuration Utilities
162
+
163
+ Helper functions for validating and working with `RedisConfig`:
164
+
165
+ | Function | Signature | Description |
166
+ | :--- | :--- | :--- |
167
+ | `validateRedisConfig` | `(config: RedisConfig) => Effect<RedisConfig, RedisConnectionError>` | Validates host/port ranges, URL format |
168
+ | `mergeWithDefaults` | `(config?: Partial<RedisConfig>) => RedisConfig` | Fills missing fields with defaults |
169
+ | `getConnectionInfo` | `(config: RedisConfig) => string` | Returns a safe-to-log connection string (no credentials) |
170
+ | `DEFAULT_REDIS_CONFIG` | `{ host: 'localhost', port: 6379, connectTimeout: 10000, commandTimeout: 5000 }` | Default values |
171
+
172
+ ---
173
+
174
+ ## Redis Service (Core)
175
+
176
+ The `Redis` service provides typed wrappers for common Redis commands. Every
177
+ method returns `Effect<T, RedisError>`.
178
+
179
+ ### Key-Value
180
+
181
+ | Method | Signature | Description |
182
+ | :--- | :--- | :--- |
183
+ | `get` | `(key: string) => Effect<string \| null>` | Get the value of a key |
184
+ | `set` | `(key: string, value: RedisValue, options?: KeyOptions) => Effect<'OK' \| string \| null>` | Set a key to a value with optional expiration, condition, and GET |
185
+ | `del` | `(...keys: string[]) => Effect<number>` | Delete one or more keys; returns the number of keys removed |
186
+ | `exists` | `(...keys: string[]) => Effect<number>` | Check how many of the given keys exist |
187
+ | `expire` | `(key: string, seconds: number) => Effect<number>` | Set a timeout on a key in seconds |
188
+ | `pexpire` | `(key: string, milliseconds: number) => Effect<number>` | Set a timeout on a key in milliseconds |
189
+ | `ttl` | `(key: string) => Effect<number>` | Get remaining time-to-live in seconds (`-1` if no expiry, `-2` if missing) |
190
+ | `pttl` | `(key: string) => Effect<number>` | Get remaining time-to-live in milliseconds |
191
+ | `incr` | `(key: string) => Effect<number>` | Atomically increment the integer value of a key by 1 |
192
+ | `decr` | `(key: string) => Effect<number>` | Atomically decrement the integer value of a key by 1 |
193
+ | `incrby` | `(key: string, increment: number) => Effect<number>` | Increment the integer value of a key by a given amount |
194
+ | `decrby` | `(key: string, decrement: number) => Effect<number>` | Decrement the integer value of a key by a given amount |
195
+
196
+ ### Hash
197
+
198
+ | Method | Signature | Description |
199
+ | :--- | :--- | :--- |
200
+ | `hset` | `(key: string, field: string, value: RedisHashValue) => Effect<number>` | Set a field in a hash |
201
+ | `hget` | `(key: string, field: string) => Effect<string \| null>` | Get the value of a hash field |
202
+ | `hgetall` | `(key: string) => Effect<Record<string, string>>` | Get all fields and values of a hash |
203
+ | `hdel` | `(key: string, ...fields: string[]) => Effect<number>` | Delete one or more hash fields |
204
+ | `hexists` | `(key: string, field: string) => Effect<boolean>` | Check if a hash field exists |
205
+ | `hkeys` | `(key: string) => Effect<string[]>` | Get all field names in a hash |
206
+ | `hvals` | `(key: string) => Effect<string[]>` | Get all values in a hash |
207
+ | `hlen` | `(key: string) => Effect<number>` | Get the number of fields in a hash |
208
+
209
+ ### List
210
+
211
+ | Method | Signature | Description |
212
+ | :--- | :--- | :--- |
213
+ | `lpush` | `(key: string, ...values: RedisListValue[]) => Effect<number>` | Prepend one or more values to a list |
214
+ | `rpush` | `(key: string, ...values: RedisListValue[]) => Effect<number>` | Append one or more values to a list |
215
+ | `lpop` | `(key: string, count?: number) => Effect<string \| string[] \| null>` | Remove and return element(s) from the head of a list |
216
+ | `rpop` | `(key: string, count?: number) => Effect<string \| string[] \| null>` | Remove and return element(s) from the tail of a list |
217
+ | `lrange` | `(key: string, start: number, stop: number) => Effect<string[]>` | Get a range of elements from a list |
218
+ | `llen` | `(key: string) => Effect<number>` | Get the length of a list |
219
+ | `lrem` | `(key: string, count: number, element: RedisListValue) => Effect<number>` | Remove elements matching a value from a list |
220
+
221
+ ### Set
222
+
223
+ | Method | Signature | Description |
224
+ | :--- | :--- | :--- |
225
+ | `sadd` | `(key: string, ...members: RedisSetMember[]) => Effect<number>` | Add one or more members to a set |
226
+ | `srem` | `(key: string, ...members: RedisSetMember[]) => Effect<number>` | Remove one or more members from a set |
227
+ | `sismember` | `(key: string, member: RedisSetMember) => Effect<boolean>` | Check if a value is a member of a set |
228
+ | `smembers` | `(key: string) => Effect<string[]>` | Get all members of a set |
229
+ | `scard` | `(key: string) => Effect<number>` | Get the number of members in a set |
230
+
231
+ ### Sorted Set
232
+
233
+ | Method | Signature | Description |
234
+ | :--- | :--- | :--- |
235
+ | `zadd` | `(key: string, score: number, member: string, ...rest: (number \| string)[]) => Effect<number>` | Add one or more members with scores to a sorted set |
236
+ | `zrange` | `(key: string, start: number, stop: number, withScores?: boolean) => Effect<string[] \| { value: string; score: number }[]>` | Get a range of members by index; optionally include scores |
237
+ | `zrangebyscore` | `(key: string, min: number \| string, max: number \| string, withScores?: boolean) => Effect<string[] \| { value: string; score: number }[]>` | Get members with scores within a given range |
238
+ | `zscore` | `(key: string, member: string) => Effect<number \| null>` | Get the score of a member in a sorted set |
239
+ | `zrem` | `(key: string, ...members: string[]) => Effect<number>` | Remove one or more members from a sorted set |
240
+ | `zcard` | `(key: string) => Effect<number>` | Get the number of members in a sorted set |
241
+
242
+ ### Admin & Utilities
243
+
244
+ | Method | Signature | Description |
245
+ | :--- | :--- | :--- |
246
+ | `scan` | `(options?: ScanOptions) => Effect<ScanResult>` | Incrementally iterate over keys with optional pattern and type filtering |
247
+ | `ping` | `(message?: string) => Effect<string>` | Test connection liveness; returns `PONG` or the provided message |
248
+ | `dbsize` | `() => Effect<number>` | Get the number of keys in the current database |
249
+ | `flushdb` | `() => Effect<'OK'>` | Remove all keys from the current database |
250
+ | `flushall` | `() => Effect<'OK'>` | Remove all keys from all databases |
251
+ | `quit` | `() => Effect<'OK'>` | Gracefully close the connection |
252
+ | `disconnect` | `() => Effect<void>` | Force-close the connection immediately |
253
+
254
+ ### Raw Access
255
+
256
+ | Method | Signature | Description |
257
+ | :--- | :--- | :--- |
258
+ | `use` | `<T>(fn: (client: RedisClient) => T) => Effect<Awaited<T>>` | Execute any operation on the raw `node-redis` client |
259
+ | `execute` | `<A>(command: string, ...args: (string \| number \| Buffer)[]) => Effect<A>` | Send a raw Redis command |
260
+ | `multi` | `(commands: [string, ...(string \| number \| Buffer)[]][]) => Effect<unknown[]>` | Execute a MULTI/EXEC transaction |
261
+
262
+ ---
263
+
264
+ ## RedisPubSub Service
265
+
266
+ Provides publish/subscribe messaging with **separate connections** for publish
267
+ (shared) and subscribe (dedicated) to avoid blocking.
118
268
 
269
+ | Method | Signature | Description |
270
+ | :--- | :--- | :--- |
271
+ | `publish` | `(channel: string, message: string) => Effect<void>` | Publish a message to a channel |
272
+ | `subscribe` | `(channel: string) => Effect<Stream<string>>` | Subscribe to a channel; returns a continuous `Stream` of messages |
273
+
274
+ ### Connection Model
275
+
276
+ - **Publish**: reuses the shared `RedisConnection` if available
277
+ - **Subscribe**: always creates a dedicated connection (Redis requires a
278
+ separate client for blocking subscribe operations)
279
+
280
+ ---
281
+
282
+ ## RedisStream Service
283
+
284
+ Provides Redis Streams operations with **separate producer/consumer
285
+ connections**. The producer reuses the shared connection; the consumer always
286
+ gets a dedicated client for blocking reads.
287
+
288
+ | Method | Signature | Description |
289
+ | :--- | :--- | :--- |
290
+ | `xadd` | `(key, id, message) => Effect<string>` | Append an entry to a stream; use `'*'` for auto-generated IDs |
291
+ | `xread` | `(key, id, options?) => Effect<StreamEntry[]>` | Read entries from a stream (supports blocking) |
292
+ | `xrange` | `(key, start, end, options?) => Effect<StreamEntry[]>` | Read a range of entries (`'-'` to `'+'` for all) |
293
+ | `subscribe` | `(key, options?) => Stream<StreamEntry>` | Continuous polling stream (blocks and yields new entries) |
294
+ | `xack` | `(key, group, ...ids) => Effect<number>` | Acknowledge processed entries in a consumer group |
295
+
296
+ ### `StreamEntry`
297
+
298
+ ```ts
299
+ interface StreamEntry {
300
+ id: RedisArgument; // e.g. "1234567890-0"
301
+ data: Record<string, string>;
302
+ }
303
+ ```
304
+
305
+ ### `StreamSubscribeOptions`
306
+
307
+ ```ts
308
+ interface StreamSubscribeOptions {
309
+ readonly id?: string; // Start ID (default: '$' — new entries only)
310
+ readonly block?: number; // Block time in ms (default: 5000)
311
+ readonly count?: number; // Max entries per read
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Error Model
318
+
319
+ Every operation fails with `RedisError`, a discriminated union of three tagged
320
+ error types. All extend `Data.TaggedError` for pattern matching with
321
+ `Effect.catchTag`.
322
+
323
+ ```ts
324
+ class RedisConnectionError extends Data.TaggedError('RedisConnectionError')<{
325
+ readonly cause: unknown;
326
+ readonly message: string;
327
+ }> {}
328
+
329
+ class RedisCommandError extends Data.TaggedError('RedisCommandError')<{
330
+ readonly cause: unknown;
331
+ readonly message: string;
332
+ readonly command: string;
333
+ }> {}
334
+
335
+ class RedisGeneralError extends Data.TaggedError('RedisGeneralError')<{
336
+ readonly cause: unknown;
337
+ readonly message: string;
338
+ }> {}
339
+
340
+ type RedisError = RedisConnectionError | RedisCommandError | RedisGeneralError;
341
+ ```
342
+
343
+ | Error | Tag | Fields | When |
344
+ | :--- | :--- | :--- | :--- |
345
+ | `RedisConnectionError` | `'RedisConnectionError'` | `cause`, `message` | Connection failures (ECONNREFUSED, ETIMEDOUT, etc.) |
346
+ | `RedisCommandError` | `'RedisCommandError'` | `cause`, `message`, `command` | Command execution failures (WRONGTYPE, NOAUTH, etc.) |
347
+ | `RedisGeneralError` | `'RedisGeneralError'` | `cause`, `message` | All other Redis-related failures |
348
+
349
+ The `toRedisError` utility automatically classifies raw errors into the correct
350
+ type using error codes and message pattern matching.
351
+
352
+ ```ts
353
+ import { Effect } from 'effect';
354
+ import { Redis } from 'effect-redis';
355
+
356
+ const program = Effect.gen(function* () {
357
+ const redis = yield* Redis;
358
+ yield* redis.get('my-key').pipe(
359
+ Effect.catchTag('RedisConnectionError', (e) =>
360
+ Effect.logError(`Connection lost: ${e.message}`)
361
+ ),
362
+ Effect.catchTag('RedisCommandError', (e) =>
363
+ Effect.logError(`Command ${e.command} failed: ${e.message}`)
364
+ )
365
+ );
366
+ });
367
+ ```
368
+
369
+ ---
370
+
371
+ ## Types Reference
372
+
373
+ ### Value Types
374
+
375
+ | Type | Definition |
376
+ | :--- | :--- |
377
+ | `RedisValue` | `string \| number \| Buffer` |
378
+ | `RedisHashValue` | `string \| number \| Buffer \| Record<string, string \| number \| Buffer>` |
379
+ | `RedisSetMember` | `string \| Buffer` |
380
+ | `RedisListValue` | `string \| number \| Buffer` |
381
+
382
+ ### `KeyOptions` (for `set`)
383
+
384
+ ```ts
385
+ interface KeyOptions {
386
+ readonly expiration?: {
387
+ readonly mode: 'EX' | 'PX' | 'EXAT' | 'PXAT';
388
+ readonly time: number;
389
+ };
390
+ readonly condition?: 'NX' | 'XX';
391
+ readonly get?: boolean; // When true, SET returns the old value
392
+ }
393
+ ```
394
+
395
+ ### `ScanOptions` / `ScanResult`
396
+
397
+ ```ts
398
+ interface ScanOptions {
399
+ readonly cursor?: string;
400
+ readonly match?: string; // Glob pattern (e.g. 'user:*')
401
+ readonly count?: number;
402
+ readonly type?: 'string' | 'list' | 'set' | 'zset' | 'hash' | 'stream';
403
+ }
404
+
405
+ interface ScanResult {
406
+ readonly cursor: string;
407
+ readonly keys: readonly string[];
408
+ }
409
+ ```
410
+
411
+ ### `RedisConfig`
412
+
413
+ ```ts
414
+ type RedisConfig =
415
+ | { host: string; port: number; username?: string; password?: string;
416
+ database?: number; ssl?: boolean; connectTimeout?: number;
417
+ commandTimeout?: number; }
418
+ | { url: string; connectTimeout?: number; commandTimeout?: number; };
419
+ ```
119
420
 
120
421
  ---
121
422
 
122
423
  ## Usage Examples
123
424
 
124
- ### 1. Automatic Reconnection & Retries
125
- In production, connections can drop. Use `Stream.retry` with a `Schedule` to handle this gracefully:
425
+ ### 1. Retry with Exponential Backoff
126
426
 
127
427
  ```ts
128
- import { Effect, Stream, Schedule, Duration } from "effect";
129
- import { RedisPubSub } from "effect-redis";
428
+ import { Effect, Stream, Schedule, Duration } from 'effect';
429
+ import { RedisPubSub } from 'effect-redis';
130
430
 
131
431
  const program = Effect.gen(function* () {
132
432
  const pubsub = yield* RedisPubSub;
133
- const subscription = yield* pubsub.subscribe("raw-data");
433
+ const subscription = yield* pubsub.subscribe('raw-data');
134
434
 
135
- const monitoringStream = subscription.pipe(
136
- Stream.tap(msg => Effect.log(`Received: ${msg}`)),
137
- // Retry with exponential backoff if the connection fails
435
+ yield* subscription.pipe(
436
+ Stream.tap((msg) => Effect.log(`Received: ${msg}`)),
138
437
  Stream.retry(
139
438
  Schedule.exponential(Duration.seconds(1)).pipe(
140
439
  Schedule.intersect(Schedule.recurs(5))
141
440
  )
142
- )
441
+ ),
442
+ Stream.runDrain
143
443
  );
144
-
145
- yield* Stream.runDrain(monitoringStream);
146
444
  });
147
445
  ```
148
446
 
149
- ### 2. Stream Processing with Schema Validation
150
- Integrate with `@effect/schema` to build type-safe data pipelines:
447
+ ### 2. Schema-Validated Stream Processing
151
448
 
152
449
  ```ts
153
- import { Effect, Stream, Schema, pipe } from "effect";
154
- import { RedisPubSub } from "effect-redis";
450
+ import { Effect, Stream, Schema, pipe } from 'effect';
451
+ import { RedisPubSub } from 'effect-redis';
155
452
 
156
453
  const TradeSchema = Schema.Struct({
157
454
  symbol: Schema.String,
@@ -160,89 +457,92 @@ const TradeSchema = Schema.Struct({
160
457
 
161
458
  const program = Effect.gen(function* () {
162
459
  const pubsub = yield* RedisPubSub;
163
- const trades = yield* pubsub.subscribe("trades");
460
+ const trades = yield* pubsub.subscribe('trades');
164
461
 
165
462
  yield* pipe(
166
463
  trades,
167
464
  Stream.map(JSON.parse),
168
- Stream.mapEffect(data =>
465
+ Stream.mapEffect((data) =>
169
466
  Schema.decodeUnknown(TradeSchema)(data).pipe(
170
- Effect.tapError(err => Effect.logWarning(`Invalid trade: ${err}`)),
171
- Effect.option // Filter out invalid entries
467
+ Effect.tapError((err) => Effect.logWarning(`Invalid trade: ${err}`)),
468
+ Effect.option
172
469
  )
173
470
  ),
174
- Stream.filterMap(opt => opt),
175
- Stream.tap(trade => Effect.log(`Valid trade: ${trade.symbol} @ ${trade.price}`)),
471
+ Stream.filterMap((opt) => opt),
472
+ Stream.tap((trade) =>
473
+ Effect.log(`Valid trade: ${trade.symbol} @ ${trade.price}`)
474
+ ),
176
475
  Stream.runDrain
177
476
  );
178
477
  });
179
478
  ```
180
479
 
181
- ### 3. Custom Commands & Transactions
182
- Escalate to the raw `node-redis` client for features not natively wrapped:
480
+ ### 3. Raw Client & Transactions
183
481
 
184
482
  ```ts
185
- import { Effect } from "effect";
186
- import { Redis } from "effect-redis";
483
+ import { Effect } from 'effect';
484
+ import { Redis } from 'effect-redis';
187
485
 
188
486
  const program = Effect.gen(function* () {
189
487
  const redis = yield* Redis;
190
488
 
191
- // Execute a transaction using the raw client
192
489
  const results = yield* redis.use((client) =>
193
- client
194
- .multi()
195
- .set("key1", "val1")
196
- .set("key2", "val2")
197
- .exec()
490
+ client.multi().set('key1', 'val1').set('key2', 'val2').exec()
198
491
  );
199
492
 
200
493
  yield* Effect.log(`Transaction results: ${JSON.stringify(results)}`);
201
494
  });
202
495
  ```
203
496
 
204
- ### 4. Continuous Redis Stream Polling
205
- Use `RedisStream.subscribe` for continuous polling. You can track the last seen ID to ensure continuity across restarts:
497
+ ### 4. Continuous Stream Polling
206
498
 
207
499
  ```ts
208
- import { Effect, Stream } from "effect";
209
- import { RedisStream } from "effect-redis";
500
+ import { Effect, Stream, Ref } from 'effect';
501
+ import { RedisStream } from 'effect-redis';
210
502
 
211
503
  const program = Effect.gen(function* () {
212
- const redisStream = yield* RedisStream;
213
-
214
- // Track the last seen ID in a Ref for continuity
215
- const lastId = yield* Effect.makeRef("$");
216
-
217
- const events = redisStream.subscribe("app-events", {
218
- id: yield* lastId.get,
219
- block: 5000 // Wait up to 5s if idle
220
- }).pipe(
221
- Stream.tap(entry => lastId.set(String(entry.id)))
222
- );
223
-
224
- yield* Stream.runForEach(events, entry =>
225
- Effect.log(`Event ID: ${entry.id}, Data: ${JSON.stringify(entry.message)}`)
504
+ const stream = yield* RedisStream;
505
+ const lastId = yield* Ref.make('$');
506
+
507
+ const events = stream
508
+ .subscribe('app-events', {
509
+ id: yield* Ref.get(lastId),
510
+ block: 5000,
511
+ })
512
+ .pipe(Stream.tap((entry) => Ref.set(lastId, String(entry.id))));
513
+
514
+ yield* Stream.runForEach(events, (entry) =>
515
+ Effect.log(`Event ${entry.id}: ${JSON.stringify(entry.data)}`)
226
516
  );
227
517
  });
228
518
  ```
229
519
 
230
- ---
520
+ ### 5. SET with Expiration and Conditions
231
521
 
232
- ## Error Model
522
+ ```ts
523
+ import { Effect } from 'effect';
524
+ import { Redis } from 'effect-redis';
233
525
 
234
- Every operation can fail with a `RedisError`, which is a union of specific error types:
235
- - `RedisConnectionError`: Failures during connection.
236
- - `RedisCommandError`: Failures during command execution.
237
- - `RedisGeneralError`: Other Redis-related failures.
526
+ const program = Effect.gen(function* () {
527
+ const redis = yield* Redis;
238
528
 
239
- All errors are tagged and include the original `cause`.
529
+ // Set only if key does not exist, expire in 60 seconds
530
+ yield* redis.set('lock:resource', 'owner-1', {
531
+ condition: 'NX',
532
+ expiration: { mode: 'EX', time: 60 },
533
+ });
240
534
 
241
- ```ts
242
- const program = Redis.pipe(
243
- Effect.flatMap(r => r.get("my-key")),
244
- Effect.catchTag("RedisCommandError", (e) =>
245
- Effect.logError(`Command failed: ${e.command}`, e.cause)
246
- )
247
- );
535
+ // Set and return the previous value
536
+ const oldValue = yield* redis.set('config:version', '2', { get: true });
537
+ });
248
538
  ```
539
+
540
+ ---
541
+
542
+ ## Peer Dependencies
543
+
544
+ | Package | Version |
545
+ | :--- | :--- |
546
+ | `redis` | `^5.1.0` |
547
+ | `typescript` | `^5` |
548
+ | `effect` | (provided via workspace catalog) |