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.
- package/README.md +180 -180
- package/dist/examples/stream-read-write.d.ts +1 -0
- package/dist/examples/stream.d.ts +1 -0
- package/dist/index.js +7 -7
- package/dist/src/index.d.ts +1 -0
- package/dist/src/redis.d.ts +57 -0
- package/package.json +7 -5
- package/src/redis.ts +220 -59
|
@@ -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.
|
|
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
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
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
|
|
133
|
+
const bootstrapRedisServiceEffect = Effect.gen(function* () {
|
|
127
134
|
const client = yield* redisClientEffect;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
setValue: (key, value) =>
|
|
135
|
+
return Redis.of({
|
|
136
|
+
use: (fn) =>
|
|
131
137
|
Effect.gen(function* () {
|
|
132
|
-
|
|
133
|
-
try: () => client
|
|
138
|
+
const result = yield* Effect.try({
|
|
139
|
+
try: () => fn(client),
|
|
134
140
|
catch: (e) =>
|
|
135
141
|
new RedisError({
|
|
136
142
|
cause: e,
|
|
137
|
-
message: '
|
|
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
|
-
|
|
181
|
+
bootstrapRedisPersistenceServiceEffect,
|
|
147
182
|
);
|
|
148
183
|
|
|
149
|
-
const
|
|
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.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
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
|
};
|