effect-redis 0.0.2 → 0.0.4

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 CHANGED
@@ -1,15 +1,180 @@
1
- # effect-redis
2
-
3
- To install dependencies:
4
-
5
- ```bash
6
- bun install
7
- ```
8
-
9
- To run:
10
-
11
- ```bash
12
- bun run index.ts
13
- ```
14
-
15
- This project was created using `bun init` in bun v1.2.14. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
1
+ # Effect-Redis
2
+
3
+ A lightweight wrapper around the official `redis` client that integrates seamlessly with the [Effect](https://github.com/Effect-TS/effect) ecosystem.
4
+
5
+ * Resource–safe connections (acquire / release handled for you)
6
+ * _Layers_ for dependency-injection
7
+ * Tagged error type (`RedisError`) with rich cause information
8
+ * Declarative, interrupt-safe `publish` / `subscribe` / `setValue` helpers
9
+ * Tiny surface — bring your own redis commands via the `Redis` service when you need full power
10
+
11
+ ---
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add effect-redis redis @effect/data @effect/io
17
+ ```
18
+
19
+ > The library itself is runtime-agnostic. Under Bun you will usually also have `@effect/platform-bun` around — that is **not** required by `effect-redis`.
20
+
21
+ ---
22
+
23
+ ## Quick-start
24
+
25
+ ```ts
26
+ import { Effect, Layer } from "effect";
27
+ import {
28
+ RedisConnectionOptionsLive,
29
+ RedisPubSubLive,
30
+ RedisPersistenceLive,
31
+ RedisPubSub,
32
+ RedisPersistence,
33
+ } from "effect-redis";
34
+
35
+ const redisLayer = RedisConnectionOptionsLive({
36
+ url: "redis://localhost:6379",
37
+ }).pipe(Layer.provideMerge(RedisPubSubLive), Layer.provideMerge(RedisPersistenceLive));
38
+
39
+ const program = Effect.gen(function* () {
40
+ /* services */
41
+ const pubsub = yield* RedisPubSub;
42
+ const storage = yield* RedisPersistence;
43
+
44
+ /* persistence */
45
+ yield* storage.setValue("user:42", JSON.stringify({ name: "Ada" }));
46
+
47
+ /* pub / sub */
48
+ yield* pubsub.subscribe("notifications", (msg) => {
49
+ console.log("🔔", msg);
50
+ });
51
+
52
+ yield* pubsub.publish("notifications", "Hello world!");
53
+ });
54
+
55
+ Effect.runPromise(program.pipe(Effect.provide(redisLayer)));
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Provided Layers & Services
61
+
62
+ | Layer | Service | What you get |
63
+ | ---------------------------------------- | ---------------------- | --------------------------------------------- |
64
+ | `RedisConnectionOptionsLive(options?)` | — | Supplies connection settings downstream |
65
+ | `RedisLive` | `Redis` | Raw access to an already connected client |
66
+ | `RedisPubSubLive` | `RedisPubSub` | `publish / subscribe` helpers |
67
+ | `RedisPersistenceLive` | `RedisPersistence` | Simple `setValue` helper |
68
+
69
+ All Layers are *scoped* — the underlying connection is opened once and **closed automatically** when the scope ends.
70
+
71
+ ### Error model
72
+
73
+ Every operation can fail with **`RedisError`** — a tagged error enriched with the original cause:
74
+
75
+ ```ts
76
+ import { Effect } from "effect";
77
+ import { RedisPersistence, RedisError } from "effect-redis";
78
+
79
+ const safe = RedisPersistence.pipe(
80
+ Effect.flatMap(({ setValue }) => setValue("x", "y")),
81
+ Effect.catchTag("RedisError", (e: RedisError) => Effect.logError(e.message))
82
+ );
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Usage patterns
88
+
89
+ ### 1. Pub / Sub micro-service
90
+
91
+ Extracted from the repo’s own `winfut.ts` example:
92
+
93
+ ```ts
94
+ import { BunRuntime } from "@effect/platform-bun";
95
+ import { Effect, Queue, Stream, pipe } from "effect";
96
+ import { RedisConnectionOptionsLive, RedisPubSubLive, RedisPubSub } from "effect-redis";
97
+
98
+ const program = Effect.gen(function* () {
99
+ const q = yield* Queue.unbounded<string>();
100
+ const rps = yield* RedisPubSub;
101
+
102
+ /* subscribe */
103
+ yield* rps.subscribe("raw", (msg) => Queue.unsafeOffer(q, msg));
104
+
105
+ /* process stream */
106
+ yield* pipe(
107
+ Stream.fromQueue(q),
108
+ Stream.filter((m) => m.startsWith("T:WIN")),
109
+ Stream.tap((m) => rps.publish("winfut", m)),
110
+ Stream.runDrain
111
+ );
112
+ });
113
+
114
+ BunRuntime.runMain(
115
+ Effect.provide(
116
+ program,
117
+ Layer.provide(RedisPubSubLive, RedisConnectionOptionsLive({ url: "redis://localhost:6379" }))
118
+ )
119
+ );
120
+ ```
121
+
122
+ ### 2. Metrics aggregation
123
+
124
+ See `metrics.ts` for the full blown version, the gist is:
125
+
126
+ ```ts
127
+ const metricsProg = Effect.gen(function* () {
128
+ const rps = yield* RedisPubSub;
129
+ yield* rps.subscribe("winfut", handleTick);
130
+ // … aggregate & publish …
131
+ });
132
+ ```
133
+
134
+ ### 3. Low-level commands via `Redis`
135
+
136
+ The thin `Redis` service gives you the connected **node-redis client** when you need functions not wrapped by this lib:
137
+
138
+ ```ts
139
+ import { Redis } from "effect-redis";
140
+
141
+ const incrProg = Redis.pipe(
142
+ Effect.flatMap(({ use }) =>
143
+ use((client) => client.incr("counter"))
144
+ )
145
+ );
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Reference
151
+
152
+ ### `RedisConnectionOptionsLive(options?) → Layer`
153
+ Creates a live Layer that exposes `RedisConnectionOptions` downstream. The `options` object is forwarded to `createClient(options)` from `redis`.
154
+
155
+ ### `RedisPubSub` service
156
+ ```ts
157
+ publish(channel: string, message: string): Effect<void, RedisError>
158
+ subscribe(channel: string, handler: (msg: string) => void): Effect<void, RedisError>
159
+ ```
160
+
161
+ ### `RedisPersistence` service
162
+ ```ts
163
+ setValue(key: string, value: string): Effect<void, RedisError>
164
+ ```
165
+
166
+ ### `Redis` service
167
+ ```ts
168
+ use<T>(fn: (client: RedisClient) => T | Promise<T>): Effect<T, RedisError>
169
+ ```
170
+ Gives access to the raw `RedisClient` from `redis`. The connection is shared and stays open for the whole scope.
171
+
172
+ ---
173
+
174
+ ## Contributing / TODO
175
+
176
+ * Expose more common redis commands (get, del, etc.)
177
+ * Connection pooling?
178
+ * Add tests against `redis-mock`
179
+
180
+ PRs welcome!
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "effect-redis",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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", "README.md"],
9
+ "files": ["dist", "src", "README.md"],
10
10
  "scripts": {
11
11
  "build:main": "bun build --minify-syntax --minify-whitespace ./src/index.ts --outdir ./dist --target node --format esm",
12
12
  "build:types": "bun tsc --emitDeclarationOnly --outDir dist",
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './redis';
package/src/redis.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { Context, Data, Effect, Layer } from 'effect';
2
+ import { createClient } from 'redis';
3
+
4
+ export class RedisError extends Data.TaggedError('RedisError')<{
5
+ cause: unknown;
6
+ message: string;
7
+ }> {}
8
+
9
+ interface RedisConnectionOptionsShape {
10
+ options?: Parameters<typeof createClient>[0];
11
+ }
12
+ class RedisConnectionOptions extends Context.Tag('RedisConnectionOptions')<
13
+ RedisConnectionOptions,
14
+ RedisConnectionOptionsShape
15
+ >() {}
16
+
17
+ const RedisConnectionOptionsLive = (
18
+ options?: Parameters<typeof createClient>[0],
19
+ ) =>
20
+ Layer.succeed(
21
+ RedisConnectionOptions,
22
+ RedisConnectionOptions.of({
23
+ options,
24
+ }),
25
+ );
26
+
27
+ interface RedisShape {
28
+ use: <T>(
29
+ fn: (client: ReturnType<typeof createClient>) => T,
30
+ ) => Effect.Effect<Awaited<T>, RedisError, never>;
31
+ }
32
+ class Redis extends Context.Tag('Redis')<Redis, RedisShape>() {}
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
+ interface RedisPubSubShape {
65
+ publish: (
66
+ channel: string,
67
+ message: string,
68
+ ) => Effect.Effect<void, RedisError, never>;
69
+ subscribe: (
70
+ channel: string,
71
+ handler: (message: string) => void,
72
+ ) => Effect.Effect<void, RedisError, never>;
73
+ }
74
+
75
+ class RedisPubSub extends Context.Tag('RedisPubSub')<
76
+ RedisPubSub,
77
+ RedisPubSubShape
78
+ >() {}
79
+
80
+ interface RedisPersistenceShape {
81
+ setValue: (
82
+ key: string,
83
+ value: string,
84
+ ) => Effect.Effect<void, RedisError, never>;
85
+ }
86
+
87
+ class RedisPersistence extends Context.Tag('RedisPersistence')<
88
+ RedisPersistence,
89
+ RedisPersistenceShape
90
+ >() {}
91
+
92
+ const redisClientEffect = Effect.gen(function* () {
93
+ const { options } = yield* RedisConnectionOptions;
94
+
95
+ return yield* Effect.acquireRelease(
96
+ Effect.tryPromise({
97
+ try: () =>
98
+ createClient(options)
99
+ .connect()
100
+ .then((r) => {
101
+ console.log('Connected to Redis');
102
+ r.on('error', (e) => {
103
+ console.log('Redis error(on error):', e.message);
104
+ r.destroy();
105
+ });
106
+ r.on('end', () => {
107
+ console.log('Connection to Redis ended');
108
+ });
109
+ return r;
110
+ }),
111
+ catch: (e) =>
112
+ new RedisError({
113
+ cause: e,
114
+ message: 'Error while connecting to Redis',
115
+ }),
116
+ }),
117
+ (client) =>
118
+ Effect.sync(() => {
119
+ if (client.isReady) {
120
+ client.quit();
121
+ }
122
+ }),
123
+ );
124
+ });
125
+
126
+ const bootstrapRedisPersistenceEffect = Effect.gen(function* () {
127
+ const client = yield* redisClientEffect;
128
+
129
+ return RedisPersistence.of({
130
+ setValue: (key, value) =>
131
+ Effect.gen(function* () {
132
+ return yield* Effect.tryPromise({
133
+ try: () => client.set(key, value),
134
+ catch: (e) =>
135
+ new RedisError({
136
+ cause: e,
137
+ message: 'Error in `Redis.setValue`',
138
+ }),
139
+ });
140
+ }),
141
+ });
142
+ });
143
+
144
+ const RedisPersistenceLive = Layer.scoped(
145
+ RedisPersistence,
146
+ bootstrapRedisPersistenceEffect,
147
+ );
148
+
149
+ const bootstrapRedisPubSubEffect = Effect.gen(function* () {
150
+ const clientPublish = yield* redisClientEffect;
151
+ const clientSubscribe = yield* redisClientEffect;
152
+
153
+ return RedisPubSub.of({
154
+ 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
+ });
164
+ }),
165
+ 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
+ });
175
+ }),
176
+ });
177
+ });
178
+
179
+ const RedisPubSubLive = Layer.scoped(RedisPubSub, bootstrapRedisPubSubEffect);
180
+
181
+ export {
182
+ RedisPersistence,
183
+ RedisPubSub,
184
+ RedisConnectionOptions,
185
+ Redis,
186
+ RedisPersistenceLive,
187
+ RedisPubSubLive,
188
+ RedisConnectionOptionsLive,
189
+ RedisLive,
190
+ };