effect-redis 0.0.6 → 0.0.8

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