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