effect-redis 0.0.20 → 0.0.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-redis",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Simple Effect wrapper for Redis.",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,8 @@
8
8
  "type": "module",
9
9
  "files": [
10
10
  "dist",
11
- "README.md"
11
+ "README.md",
12
+ "src"
12
13
  ],
13
14
  "scripts": {
14
15
  "build:main": "bun build --minify-syntax --minify-whitespace ./src/index.ts --outdir ./dist --target node --format esm",
@@ -29,7 +30,7 @@
29
30
  "vitest": "^4.0.16"
30
31
  },
31
32
  "peerDependencies": {
32
- "effect": "^3.19.9",
33
+ "effect": "^3.19.14",
33
34
  "redis": "^5.1.0",
34
35
  "typescript": "^5"
35
36
  },
@@ -37,4 +38,4 @@
37
38
  "@effect/experimental": "^0.58.0",
38
39
  "@effect/platform-bun": "^0.87.0"
39
40
  }
40
- }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Re-export all Redis services and utilities from the modular structure
2
+ export * from './redis';
3
+
4
+ // Re-export types
5
+ export * from './types';
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @fileoverview Redis configuration validation and utilities
3
+ */
4
+
5
+ import { Effect } from 'effect';
6
+ import type { RedisConfig } from '../types';
7
+ import { RedisConnectionError } from '../types';
8
+
9
+ /**
10
+ * Default Redis configuration values
11
+ */
12
+ export const DEFAULT_REDIS_CONFIG = {
13
+ host: 'localhost',
14
+ port: 6379,
15
+ connectTimeout: 10000,
16
+ commandTimeout: 5000,
17
+ } as const;
18
+
19
+ /**
20
+ * Validates Redis URL format
21
+ */
22
+ const isValidRedisUrl = (url: string): boolean => {
23
+ try {
24
+ const parsed = new URL(url);
25
+ return parsed.protocol === 'redis:' || parsed.protocol === 'rediss:';
26
+ } catch {
27
+ return false;
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Validates Redis host configuration
33
+ */
34
+ const validateHostConfig = (config: {
35
+ readonly host: string;
36
+ readonly port: number;
37
+ readonly username?: string;
38
+ readonly password?: string;
39
+ readonly database?: number;
40
+ readonly ssl?: boolean;
41
+ readonly connectTimeout?: number;
42
+ readonly commandTimeout?: number;
43
+ }): Effect.Effect<void, RedisConnectionError> => {
44
+ if (!config.host || config.host.trim() === '') {
45
+ return Effect.fail(
46
+ new RedisConnectionError({
47
+ cause: new Error('Invalid host'),
48
+ message: 'Redis host cannot be empty',
49
+ }),
50
+ );
51
+ }
52
+
53
+ if (config.port <= 0 || config.port > 65535) {
54
+ return Effect.fail(
55
+ new RedisConnectionError({
56
+ cause: new Error('Invalid port'),
57
+ message: `Redis port must be between 1 and 65535, got ${config.port}`,
58
+ }),
59
+ );
60
+ }
61
+
62
+ if (
63
+ config.database !== undefined &&
64
+ (config.database < 0 || config.database > 15)
65
+ ) {
66
+ return Effect.fail(
67
+ new RedisConnectionError({
68
+ cause: new Error('Invalid database'),
69
+ message: `Redis database must be between 0 and 15, got ${config.database}`,
70
+ }),
71
+ );
72
+ }
73
+
74
+ return Effect.void;
75
+ };
76
+
77
+ /**
78
+ * Validates Redis URL configuration
79
+ */
80
+ const validateUrlConfig = (config: {
81
+ readonly url: string;
82
+ readonly connectTimeout?: number;
83
+ readonly commandTimeout?: number;
84
+ }): Effect.Effect<void, RedisConnectionError> => {
85
+ if (!isValidRedisUrl(config.url)) {
86
+ return Effect.fail(
87
+ new RedisConnectionError({
88
+ cause: new Error('Invalid URL'),
89
+ message: `Invalid Redis URL format: ${config.url}`,
90
+ }),
91
+ );
92
+ }
93
+
94
+ return Effect.void;
95
+ };
96
+
97
+ /**
98
+ * Validates Redis configuration
99
+ */
100
+ export const validateRedisConfig = (
101
+ config: RedisConfig,
102
+ ): Effect.Effect<RedisConfig, RedisConnectionError> =>
103
+ Effect.gen(function* () {
104
+ if ('url' in config) {
105
+ yield* validateUrlConfig(config);
106
+ } else {
107
+ yield* validateHostConfig(config);
108
+ }
109
+ return config;
110
+ });
111
+
112
+ /**
113
+ * Merges user configuration with defaults
114
+ */
115
+ export const mergeWithDefaults = (
116
+ userConfig?: Partial<RedisConfig>,
117
+ ): RedisConfig => {
118
+ if (!userConfig) {
119
+ return DEFAULT_REDIS_CONFIG;
120
+ }
121
+
122
+ if ('url' in userConfig && userConfig.url) {
123
+ return {
124
+ url: userConfig.url,
125
+ connectTimeout:
126
+ userConfig.connectTimeout ?? DEFAULT_REDIS_CONFIG.connectTimeout,
127
+ commandTimeout:
128
+ userConfig.commandTimeout ?? DEFAULT_REDIS_CONFIG.commandTimeout,
129
+ };
130
+ }
131
+
132
+ // Type assertion to handle the union type properly
133
+ const hostConfig = userConfig as Partial<
134
+ Extract<RedisConfig, { host: string }>
135
+ >;
136
+
137
+ return {
138
+ host: hostConfig.host ?? DEFAULT_REDIS_CONFIG.host,
139
+ port: hostConfig.port ?? DEFAULT_REDIS_CONFIG.port,
140
+ username: hostConfig.username,
141
+ password: hostConfig.password,
142
+ database: hostConfig.database,
143
+ ssl: hostConfig.ssl,
144
+ connectTimeout:
145
+ hostConfig.connectTimeout ?? DEFAULT_REDIS_CONFIG.connectTimeout,
146
+ commandTimeout:
147
+ hostConfig.commandTimeout ?? DEFAULT_REDIS_CONFIG.commandTimeout,
148
+ };
149
+ };
150
+
151
+ /**
152
+ * Extracts connection info for logging (without sensitive data)
153
+ */
154
+ export const getConnectionInfo = (config: RedisConfig): string => {
155
+ if ('url' in config) {
156
+ try {
157
+ const url = new URL(config.url);
158
+ return `${url.protocol}//${url.hostname}:${url.port}${url.pathname}`;
159
+ } catch {
160
+ return 'redis://[invalid-url]';
161
+ }
162
+ }
163
+
164
+ return `redis://${config.host}:${config.port}${config.database ? `/${config.database}` : ''}`;
165
+ };
@@ -0,0 +1,69 @@
1
+ import { Context, Effect, type Scope } from 'effect';
2
+ import { createClient } from 'redis';
3
+ import type { RedisError } from '../types';
4
+ import { RedisConnectionError } from '../types';
5
+
6
+ // Shape interface for Redis connection configuration options
7
+ // Uses Parameters to extract the constructor options type from createClient
8
+ export interface RedisConnectionOptionsShape {
9
+ readonly options?: Parameters<typeof createClient>[0] | undefined;
10
+ readonly createClient?:
11
+ | ((
12
+ options?: Parameters<typeof createClient>[0],
13
+ ) => ReturnType<typeof createClient>)
14
+ | undefined;
15
+ }
16
+
17
+ // Context tag for dependency injection of Redis connection options
18
+ // Allows configuration to be provided through Effect's dependency injection system
19
+ export class RedisConnectionOptions extends Context.Tag(
20
+ 'RedisConnectionOptions',
21
+ )<RedisConnectionOptions, RedisConnectionOptionsShape>() {}
22
+
23
+ export const redisClientEffect: Effect.Effect<
24
+ ReturnType<typeof createClient>,
25
+ RedisError,
26
+ Scope.Scope | RedisConnectionOptions
27
+ > = Effect.gen(function* () {
28
+ // Retrieve Redis connection options from the context
29
+ const { options, createClient: createClientFromContext } =
30
+ yield* RedisConnectionOptions;
31
+ const create = createClientFromContext ?? createClient;
32
+
33
+ // Create a managed Redis client with automatic resource cleanup
34
+ return yield* Effect.acquireRelease(
35
+ Effect.gen(function* () {
36
+ yield* Effect.log('Connecting to Redis');
37
+ const client = yield* Effect.tryPromise({
38
+ try: () => create(options).connect(),
39
+ catch: (error) =>
40
+ new RedisConnectionError({
41
+ cause: error,
42
+ message: 'Error while connecting to Redis',
43
+ }),
44
+ });
45
+
46
+ // Set up error handling/monitoring for the Redis connection
47
+ client.on('error', (error) => {
48
+ Effect.runSync(
49
+ Effect.logError(`Redis connection error: ${error.message}`),
50
+ );
51
+ client.destroy();
52
+ });
53
+
54
+ client.on('end', () => {
55
+ Effect.runSync(Effect.logInfo('Redis connection ended'));
56
+ });
57
+
58
+ return client;
59
+ }),
60
+ // Cleanup function: properly close Redis connection when scope exits
61
+ (client) =>
62
+ Effect.gen(function* () {
63
+ yield* Effect.log('Cleaning up Redis connection');
64
+ yield* Effect.tryPromise(() => client.close()).pipe(
65
+ Effect.catchAll(() => Effect.void),
66
+ );
67
+ }),
68
+ );
69
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Redis constants and configuration values
3
+ */
4
+
5
+ /**
6
+ * Default timeout values (in milliseconds)
7
+ */
8
+ export const TIMEOUTS = {
9
+ DEFAULT_CONNECT: 10000,
10
+ DEFAULT_COMMAND: 5000,
11
+ DEFAULT_STREAM_BLOCK: 5000,
12
+ } as const;
13
+
14
+ /**
15
+ * Default Redis connection values
16
+ */
17
+ export const DEFAULTS = {
18
+ HOST: 'localhost',
19
+ PORT: 6379,
20
+ DATABASE: 0,
21
+ STREAM_ID: '$',
22
+ } as const;
23
+
24
+ /**
25
+ * Redis command names for error reporting
26
+ */
27
+ export const COMMANDS = {
28
+ GET: 'GET',
29
+ SET: 'SET',
30
+ DEL: 'DEL',
31
+ EXISTS: 'EXISTS',
32
+ EXPIRE: 'EXPIRE',
33
+ TTL: 'TTL',
34
+ HSET: 'HSET',
35
+ HGET: 'HGET',
36
+ HGETALL: 'HGETALL',
37
+ HDEL: 'HDEL',
38
+ HEXISTS: 'HEXISTS',
39
+ LPUSH: 'LPUSH',
40
+ RPUSH: 'RPUSH',
41
+ LPOP: 'LPOP',
42
+ RPOP: 'RPOP',
43
+ LRANGE: 'LRANGE',
44
+ SADD: 'SADD',
45
+ SREM: 'SREM',
46
+ SMEMBERS: 'SMEMBERS',
47
+ ZADD: 'ZADD',
48
+ ZRANGE: 'ZRANGE',
49
+ ZREM: 'ZREM',
50
+ INCR: 'INCR',
51
+ DECR: 'DECR',
52
+ SCAN: 'SCAN',
53
+ PING: 'PING',
54
+ FLUSHDB: 'FLUSHDB',
55
+ FLUSHALL: 'FLUSHALL',
56
+ PUBLISH: 'PUBLISH',
57
+ SUBSCRIBE: 'SUBSCRIBE',
58
+ XADD: 'XADD',
59
+ XREAD: 'XREAD',
60
+ XRANGE: 'XRANGE',
61
+ XACK: 'XACK',
62
+ } as const;
63
+
64
+ /**
65
+ * Redis data type names
66
+ */
67
+ export const DATA_TYPES = {
68
+ STRING: 'string',
69
+ LIST: 'list',
70
+ SET: 'set',
71
+ ZSET: 'zset',
72
+ HASH: 'hash',
73
+ STREAM: 'stream',
74
+ } as const;
75
+
76
+ /**
77
+ * Connection states
78
+ */
79
+ export const CONNECTION_STATES = {
80
+ CONNECTING: 'connecting',
81
+ CONNECTED: 'connected',
82
+ DISCONNECTING: 'disconnecting',
83
+ DISCONNECTED: 'disconnected',
84
+ ERROR: 'error',
85
+ } as const;
86
+
87
+ /**
88
+ * Log messages
89
+ */
90
+ export const LOG_MESSAGES = {
91
+ CONNECTING: 'Connecting to Redis...',
92
+ CONNECTED: 'Successfully connected to Redis',
93
+ CONNECTION_ENDED: 'Redis connection ended',
94
+ CLEANING_UP: 'Cleaning up Redis connection',
95
+ CONNECTION_ERROR: 'Redis connection error',
96
+ } as const;