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.
- package/README.md +420 -120
- package/dist/index.js +1 -1
- 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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
38
|
-
yield* redis.
|
|
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
|
-
|
|
42
|
-
yield* redis.
|
|
43
|
-
yield* redis.incr("counter");
|
|
61
|
+
yield* redis.set('counter', '1');
|
|
62
|
+
yield* redis.incr('counter');
|
|
44
63
|
|
|
45
|
-
|
|
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(
|
|
67
|
+
Effect.log(`Received: ${msg}`)
|
|
50
68
|
)
|
|
51
69
|
);
|
|
52
70
|
|
|
53
|
-
yield* pubsub.publish(
|
|
71
|
+
yield* pubsub.publish('notifications', 'Hello world!');
|
|
54
72
|
});
|
|
55
73
|
|
|
56
|
-
// Compose the layers
|
|
57
74
|
const RedisLayer = RedisConnectionOptionsLive({
|
|
58
|
-
url:
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
## Services & Layers
|
|
81
107
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
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
|
|
129
|
-
import { RedisPubSub } from
|
|
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(
|
|
433
|
+
const subscription = yield* pubsub.subscribe('raw-data');
|
|
134
434
|
|
|
135
|
-
|
|
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
|
|
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
|
|
154
|
-
import { RedisPubSub } from
|
|
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(
|
|
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
|
|
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 =>
|
|
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.
|
|
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
|
|
186
|
-
import { Redis } from
|
|
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
|
|
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
|
|
209
|
-
import { RedisStream } from
|
|
500
|
+
import { Effect, Stream, Ref } from 'effect';
|
|
501
|
+
import { RedisStream } from 'effect-redis';
|
|
210
502
|
|
|
211
503
|
const program = Effect.gen(function* () {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
522
|
+
```ts
|
|
523
|
+
import { Effect } from 'effect';
|
|
524
|
+
import { Redis } from 'effect-redis';
|
|
233
525
|
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
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) |
|