effect-redis 0.0.5 → 0.0.7

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.
@@ -0,0 +1 @@
1
+ export * from './redis';
@@ -0,0 +1,57 @@
1
+ import { Context, Effect, Layer } from 'effect';
2
+ import { type RedisArgument, createClient } from 'redis';
3
+ declare const RedisError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
4
+ readonly _tag: "RedisError";
5
+ } & Readonly<A>;
6
+ export declare class RedisError extends RedisError_base<{
7
+ cause: unknown;
8
+ message: string;
9
+ }> {
10
+ }
11
+ interface RedisConnectionOptionsShape {
12
+ options?: Parameters<typeof createClient>[0];
13
+ }
14
+ declare const RedisConnectionOptions_base: Context.TagClass<RedisConnectionOptions, "RedisConnectionOptions", RedisConnectionOptionsShape>;
15
+ declare class RedisConnectionOptions extends RedisConnectionOptions_base {
16
+ }
17
+ declare const RedisConnectionOptionsLive: (options?: Parameters<typeof createClient>[0]) => Layer.Layer<RedisConnectionOptions, never, never>;
18
+ interface RedisShape {
19
+ use: <T>(fn: (client: ReturnType<typeof createClient>) => T) => Effect.Effect<Awaited<T>, RedisError, never>;
20
+ }
21
+ declare const Redis_base: Context.TagClass<Redis, "Redis", RedisShape>;
22
+ declare class Redis extends Redis_base {
23
+ }
24
+ interface RedisPubSubShape {
25
+ publish: (channel: string, message: string) => Effect.Effect<void, RedisError, never>;
26
+ subscribe: (channel: string, handler: (message: string) => void) => Effect.Effect<void, RedisError, never>;
27
+ }
28
+ declare const RedisPubSub_base: Context.TagClass<RedisPubSub, "RedisPubSub", RedisPubSubShape>;
29
+ declare class RedisPubSub extends RedisPubSub_base {
30
+ }
31
+ interface RedisPersistenceShape {
32
+ setValue: (key: string, value: string) => Effect.Effect<void, RedisError, never>;
33
+ }
34
+ declare const RedisPersistence_base: Context.TagClass<RedisPersistence, "RedisPersistence", RedisPersistenceShape>;
35
+ declare class RedisPersistence extends RedisPersistence_base {
36
+ }
37
+ export interface StreamEntry {
38
+ id: RedisArgument;
39
+ message: Record<string, string>;
40
+ }
41
+ interface RedisStreamShape {
42
+ xadd: (key: RedisArgument, id: RedisArgument | '*', message: Record<string, RedisArgument>) => Effect.Effect<string, RedisError, never>;
43
+ xread: (key: RedisArgument, id: RedisArgument, // Use '$' to read only new entries from now
44
+ block?: number, // Block in milliseconds, 0 for indefinite
45
+ count?: number) => Effect.Effect<StreamEntry[], RedisError, never>;
46
+ xrange: (key: RedisArgument, start: RedisArgument, // '-' for earliest available
47
+ end: RedisArgument, // '+' for latest available
48
+ count?: number) => Effect.Effect<StreamEntry[], RedisError, never>;
49
+ }
50
+ declare const RedisStream_base: Context.TagClass<RedisStream, "RedisStream", RedisStreamShape>;
51
+ declare class RedisStream extends RedisStream_base {
52
+ }
53
+ declare const RedisLive: Layer.Layer<Redis, RedisError, RedisConnectionOptions>;
54
+ declare const RedisPersistenceLive: Layer.Layer<RedisPersistence, RedisError, RedisConnectionOptions>;
55
+ declare const RedisPubSubLive: Layer.Layer<RedisPubSub, RedisError, RedisConnectionOptions>;
56
+ declare const RedisStreamLive: Layer.Layer<RedisStream, RedisError, RedisConnectionOptions>;
57
+ export { RedisPersistence, RedisPubSub, RedisConnectionOptions, Redis, RedisStream, RedisPersistenceLive, RedisPubSubLive, RedisConnectionOptionsLive, RedisLive, RedisStreamLive, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-redis",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Simple Effect wrapper for Redis.",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.js",
@@ -21,9 +21,11 @@
21
21
  "@types/bun": "latest"
22
22
  },
23
23
  "peerDependencies": {
24
- "typescript": "^5",
25
- "effect": "^3.15.4",
26
- "redis": "^5.1.0"
24
+ "effect": "^3.16.2",
25
+ "redis": "^5.1.0",
26
+ "typescript": "^5"
27
27
  },
28
- "dependencies": {}
28
+ "dependencies": {
29
+ "@effect/platform-bun": "^0.69.2"
30
+ }
29
31
  }
package/src/redis.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, Data, Effect, Layer } from 'effect';
2
- import { createClient } from 'redis';
2
+ import { type RedisArgument, createClient } from 'redis';
3
3
 
4
4
  export class RedisError extends Data.TaggedError('RedisError')<{
5
5
  cause: unknown;
@@ -31,36 +31,6 @@ interface RedisShape {
31
31
  }
32
32
  class Redis extends Context.Tag('Redis')<Redis, RedisShape>() {}
33
33
 
34
- const bootstrapRedisEffect = Effect.gen(function* () {
35
- const client = yield* redisClientEffect;
36
- return Redis.of({
37
- use: (fn) =>
38
- Effect.gen(function* () {
39
- const result = yield* Effect.try({
40
- try: () => fn(client),
41
- catch: (e) =>
42
- new RedisError({
43
- cause: e,
44
- message: 'Synchronous error in `Redis.use`',
45
- }),
46
- });
47
- if (result instanceof Promise) {
48
- return yield* Effect.tryPromise({
49
- try: () => result,
50
- catch: (e) =>
51
- new RedisError({
52
- cause: e,
53
- message: 'Asynchronous error in `Redis.use`',
54
- }),
55
- });
56
- }
57
- return result;
58
- }),
59
- });
60
- });
61
-
62
- const RedisLive = Layer.scoped(Redis, bootstrapRedisEffect);
63
-
64
34
  interface RedisPubSubShape {
65
35
  publish: (
66
36
  channel: string,
@@ -89,6 +59,43 @@ class RedisPersistence extends Context.Tag('RedisPersistence')<
89
59
  RedisPersistenceShape
90
60
  >() {}
91
61
 
62
+ // Stream related types
63
+ export interface StreamEntry {
64
+ id: RedisArgument;
65
+ message: Record<string, string>;
66
+ }
67
+
68
+ interface RedisStreamShape {
69
+ // Add an entry to a stream
70
+ xadd: (
71
+ key: RedisArgument,
72
+ id: RedisArgument | '*',
73
+ message: Record<string, RedisArgument>,
74
+ ) => Effect.Effect<string, RedisError, never>;
75
+
76
+ // Read from a stream with ability to block and wait for new entries
77
+ xread: (
78
+ key: RedisArgument,
79
+ id: RedisArgument, // Use '$' to read only new entries from now
80
+ block?: number, // Block in milliseconds, 0 for indefinite
81
+ count?: number, // Max number of entries to return
82
+ ) => Effect.Effect<StreamEntry[], RedisError, never>;
83
+
84
+ // Read a range of entries from a stream
85
+ xrange: (
86
+ key: RedisArgument,
87
+ start: RedisArgument, // '-' for earliest available
88
+ end: RedisArgument, // '+' for latest available
89
+ count?: number,
90
+ ) => Effect.Effect<StreamEntry[], RedisError, never>;
91
+ }
92
+
93
+ class RedisStream extends Context.Tag('RedisStream')<
94
+ RedisStream,
95
+ RedisStreamShape
96
+ >() {}
97
+
98
+ // Common code for redis client creation
92
99
  const redisClientEffect = Effect.gen(function* () {
93
100
  const { options } = yield* RedisConnectionOptions;
94
101
 
@@ -123,68 +130,222 @@ const redisClientEffect = Effect.gen(function* () {
123
130
  );
124
131
  });
125
132
 
126
- const bootstrapRedisPersistenceEffect = Effect.gen(function* () {
133
+ const bootstrapRedisServiceEffect = Effect.gen(function* () {
127
134
  const client = yield* redisClientEffect;
128
-
129
- return RedisPersistence.of({
130
- setValue: (key, value) =>
135
+ return Redis.of({
136
+ use: (fn) =>
131
137
  Effect.gen(function* () {
132
- return yield* Effect.tryPromise({
133
- try: () => client.set(key, value),
138
+ const result = yield* Effect.try({
139
+ try: () => fn(client),
134
140
  catch: (e) =>
135
141
  new RedisError({
136
142
  cause: e,
137
- message: 'Error in `Redis.setValue`',
143
+ message: 'Synchronous error in `Redis.use`',
138
144
  }),
139
145
  });
146
+ if (result instanceof Promise) {
147
+ return yield* Effect.tryPromise({
148
+ try: () => result,
149
+ catch: (e) =>
150
+ new RedisError({
151
+ cause: e,
152
+ message: 'Asynchronous error in `Redis.use`',
153
+ }),
154
+ });
155
+ }
156
+ return result;
157
+ }),
158
+ });
159
+ });
160
+
161
+ const RedisLive = Layer.scoped(Redis, bootstrapRedisServiceEffect);
162
+
163
+ const bootstrapRedisPersistenceServiceEffect = Effect.gen(function* () {
164
+ const client = yield* redisClientEffect;
165
+
166
+ return RedisPersistence.of({
167
+ setValue: (key, value) =>
168
+ Effect.tryPromise({
169
+ try: () => client.set(key, value),
170
+ catch: (e) =>
171
+ new RedisError({
172
+ cause: e,
173
+ message: 'Error in `Redis.setValue`',
174
+ }),
140
175
  }),
141
176
  });
142
177
  });
143
178
 
144
179
  const RedisPersistenceLive = Layer.scoped(
145
180
  RedisPersistence,
146
- bootstrapRedisPersistenceEffect,
181
+ bootstrapRedisPersistenceServiceEffect,
147
182
  );
148
183
 
149
- const bootstrapRedisPubSubEffect = Effect.gen(function* () {
184
+ const bootstrapRedisPubSubServiceEffect = Effect.gen(function* () {
150
185
  const clientPublish = yield* redisClientEffect;
151
186
  const clientSubscribe = yield* redisClientEffect;
152
187
 
153
188
  return RedisPubSub.of({
154
189
  publish: (channel, message) =>
155
- Effect.gen(function* () {
156
- return yield* Effect.tryPromise({
157
- try: () => clientPublish.publish(channel, message),
158
- catch: (e) =>
159
- new RedisError({
160
- cause: e,
161
- message: 'Error in `Redis.publish`',
162
- }),
163
- });
190
+ Effect.tryPromise({
191
+ try: () => clientPublish.publish(channel, message),
192
+ catch: (e) =>
193
+ new RedisError({
194
+ cause: e,
195
+ message: 'Error in `Redis.publish`',
196
+ }),
164
197
  }),
165
198
  subscribe: (channel, handler) =>
166
- Effect.gen(function* () {
167
- return yield* Effect.tryPromise({
168
- try: () => clientSubscribe.subscribe(channel, handler),
169
- catch: (e) =>
170
- new RedisError({
171
- cause: e,
172
- message: 'Error in `Redis.subscribe`',
173
- }),
174
- });
199
+ Effect.tryPromise({
200
+ try: () => clientSubscribe.subscribe(channel, handler),
201
+ catch: (e) =>
202
+ new RedisError({
203
+ cause: e,
204
+ message: 'Error in `Redis.subscribe`',
205
+ }),
175
206
  }),
176
207
  });
177
208
  });
178
209
 
179
- const RedisPubSubLive = Layer.scoped(RedisPubSub, bootstrapRedisPubSubEffect);
210
+ const RedisPubSubLive = Layer.scoped(
211
+ RedisPubSub,
212
+ bootstrapRedisPubSubServiceEffect,
213
+ );
214
+
215
+ // Redis Stream implementation
216
+ const bootstrapRedisStreamServiceEffect = Effect.gen(function* () {
217
+ // Separate connections: one dedicated to writes (producer) and one to reads (consumer)
218
+ const clientProducer = yield* redisClientEffect;
219
+ const clientConsumer = yield* redisClientEffect;
220
+
221
+ return RedisStream.of({
222
+ xadd: (
223
+ key: RedisArgument,
224
+ id: RedisArgument,
225
+ message: Record<string, RedisArgument>,
226
+ ) =>
227
+ Effect.tryPromise({
228
+ try: async () => {
229
+ // Pass the message object directly to xAdd
230
+ return await clientProducer.xAdd(key, id, message);
231
+ },
232
+ catch: (e) =>
233
+ new RedisError({
234
+ cause: e,
235
+ message: 'Error in `RedisStream.xadd`',
236
+ }),
237
+ }),
238
+
239
+ xread: (
240
+ key: RedisArgument,
241
+ id: RedisArgument,
242
+ block?: number,
243
+ count?: number,
244
+ ) =>
245
+ Effect.tryPromise({
246
+ try: async () => {
247
+ const options: Record<string, number> = {};
248
+
249
+ if (block !== undefined) {
250
+ options.BLOCK = block;
251
+ }
252
+
253
+ if (count !== undefined) {
254
+ options.COUNT = count;
255
+ }
256
+
257
+ // Create proper XReadStream objects instead of arrays
258
+ const streams = [{ key, id }];
259
+ const result = await clientConsumer.xRead(streams, options);
260
+
261
+ if (!result) {
262
+ return [];
263
+ }
264
+
265
+ // Transform result into StreamEntry[] format
266
+ if (!Array.isArray(result)) {
267
+ return [];
268
+ }
269
+
270
+ return result.flatMap((stream) => {
271
+ // Type guard to check if stream has the expected structure
272
+ if (
273
+ stream &&
274
+ typeof stream === 'object' &&
275
+ 'messages' in stream &&
276
+ Array.isArray(stream.messages)
277
+ ) {
278
+ return stream.messages.map((msg) => {
279
+ // Add type guard for msg
280
+ if (msg && typeof msg === 'object' && 'id' in msg) {
281
+ return {
282
+ id: String(msg.id), // Convert to string explicitly
283
+ message: msg.message as Record<string, string>,
284
+ };
285
+ }
286
+ // Return a default or handle error case
287
+ return {
288
+ id: '',
289
+ message: {} as Record<string, string>,
290
+ };
291
+ });
292
+ }
293
+
294
+ return [];
295
+ });
296
+ },
297
+ catch: (e) =>
298
+ new RedisError({
299
+ cause: e,
300
+ message: 'Error in `RedisStream.xread`',
301
+ }),
302
+ }),
303
+
304
+ xrange: (
305
+ key: RedisArgument,
306
+ start: RedisArgument,
307
+ end: RedisArgument,
308
+ count?: number,
309
+ ) =>
310
+ Effect.tryPromise({
311
+ try: async () => {
312
+ const options: Record<string, number> = {};
313
+
314
+ if (count !== undefined) {
315
+ options.COUNT = count;
316
+ }
317
+
318
+ const result = await clientConsumer.xRange(key, start, end, options);
319
+
320
+ // Transform result into StreamEntry[] format
321
+ return result.map((msg) => ({
322
+ id: msg.id,
323
+ message: msg.message as Record<string, string>,
324
+ }));
325
+ },
326
+ catch: (e) =>
327
+ new RedisError({
328
+ cause: e,
329
+ message: 'Error in `RedisStream.xrange`',
330
+ }),
331
+ }),
332
+ });
333
+ });
334
+
335
+ const RedisStreamLive = Layer.scoped(
336
+ RedisStream,
337
+ bootstrapRedisStreamServiceEffect,
338
+ );
180
339
 
181
340
  export {
182
341
  RedisPersistence,
183
342
  RedisPubSub,
184
343
  RedisConnectionOptions,
185
344
  Redis,
345
+ RedisStream,
186
346
  RedisPersistenceLive,
187
347
  RedisPubSubLive,
188
348
  RedisConnectionOptionsLive,
189
349
  RedisLive,
350
+ RedisStreamLive,
190
351
  };