@ceschiatti/redistail 0.0.1 → 0.0.3

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,492 @@
1
+ /**
2
+ * Layer composition and dependency injection for the redistail CLI utility.
3
+ *
4
+ * This module provides all service layers and their composition for the redistail
5
+ * application. It follows Effect-TS patterns for dependency injection, resource
6
+ * management, and proper error handling. The layers are composed to create a
7
+ * complete application environment with all necessary services.
8
+ *
9
+ * Requirements addressed:
10
+ * - 7.1: Integration with effect-redis package services
11
+ * - 7.2: Effect-TS patterns for error handling and resource management
12
+ * - 7.3: Service composition through Effect Layers
13
+ * - 7.4: Proper error handling and resource management
14
+ */
15
+
16
+ import { Context, Effect, Layer } from 'effect';
17
+ import {
18
+ Redis,
19
+ RedisPubSub,
20
+ RedisStream,
21
+ RedisConnectionOptions,
22
+ RedisConnectionOptionsLive,
23
+ RedisLive,
24
+ RedisPubSubLive,
25
+ RedisStreamLive,
26
+ } from 'effect-redis';
27
+ import { CLIConfigService, CLIConfigServiceLive } from './config-service.js';
28
+ import {
29
+ DisplayServiceTag,
30
+ createTestDisplayService,
31
+ type DisplayService,
32
+ } from './display-service.js';
33
+ import {
34
+ PubSubMonitorService,
35
+ PubSubMonitorServiceLive,
36
+ } from './pubsub-monitor-service.js';
37
+ import {
38
+ StreamMonitorService,
39
+ StreamMonitorServiceLive,
40
+ } from './stream-monitor-service.js';
41
+ import {
42
+ SignalHandlerService,
43
+ SignalHandlerServiceLive,
44
+ } from './signal-handler-service.js';
45
+ import type { RedistailConfig, ConfigError, RedistailError } from './types.js';
46
+ import { RedistailConfig as RedistailConfigConstructor } from './types.js';
47
+
48
+ // ============================================================================
49
+ // Configuration Layer
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Context tag for RedistailConfig dependency injection
54
+ */
55
+ const RedistailConfigTag = Context.GenericTag<RedistailConfig>(
56
+ '@redistail/RedistailConfig',
57
+ );
58
+
59
+ /**
60
+ * Live layer that provides RedistailConfig by loading from environment
61
+ * This layer depends on CLIConfigService to load the configuration
62
+ */
63
+ export const RedistailConfigLive: Layer.Layer<
64
+ RedistailConfig,
65
+ ConfigError,
66
+ CLIConfigService
67
+ > = Layer.effect(
68
+ RedistailConfigTag,
69
+ Effect.gen(function* () {
70
+ const configService = yield* CLIConfigService;
71
+ return yield* configService.loadConfig();
72
+ }),
73
+ );
74
+
75
+ // ============================================================================
76
+ // Redis Connection Layer
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Live layer that provides Redis connection configuration based on RedistailConfig
81
+ * This layer creates the appropriate Redis connection options from the loaded configuration
82
+ *
83
+ * Requirement 7.1: Integrates with effect-redis RedisConnectionOptions
84
+ */
85
+ export const RedisConnectionLive: Layer.Layer<
86
+ RedisConnectionOptions,
87
+ never,
88
+ RedistailConfig
89
+ > = Layer.effect(
90
+ RedisConnectionOptions,
91
+ Effect.gen(function* () {
92
+ const config = yield* RedistailConfigTag;
93
+
94
+ // Determine the Redis URL - prefer explicit URL over host/port
95
+ const redisUrl =
96
+ config.redis.url ?? `redis://${config.redis.host}:${config.redis.port}`;
97
+
98
+ // Create Redis connection options with timeout and retry configuration
99
+ const connectionOptions = {
100
+ url: redisUrl,
101
+ socket: {
102
+ connectTimeout: config.redis.timeout,
103
+ reconnectStrategy: (retries: number) => {
104
+ // Use exponential backoff with max attempts from config
105
+ if (retries >= config.redis.retryAttempts) {
106
+ return false; // Stop retrying after max attempts
107
+ }
108
+ return Math.min(config.redis.retryDelay * 2 ** retries, 30000);
109
+ },
110
+ },
111
+ // Set command timeout
112
+ commandsQueueMaxLength: 1000,
113
+ };
114
+
115
+ // Return the connection options shape
116
+ return {
117
+ options: connectionOptions,
118
+ createClient: undefined,
119
+ };
120
+ }),
121
+ );
122
+
123
+ // ============================================================================
124
+ // Display Service Layer
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Live layer that provides DisplayService with configuration
129
+ * This layer depends on RedistailConfig to configure display options
130
+ */
131
+ export const DisplayServiceLive: Layer.Layer<
132
+ DisplayService,
133
+ never,
134
+ RedistailConfig
135
+ > = Layer.effect(
136
+ DisplayServiceTag,
137
+ Effect.gen(function* () {
138
+ const config = yield* RedistailConfigTag;
139
+ return createTestDisplayService(config);
140
+ }),
141
+ );
142
+
143
+ // ============================================================================
144
+ // Helper Functions for Layer Creation
145
+ // ============================================================================
146
+
147
+ /**
148
+ * Create a Redis connection layer with custom options
149
+ *
150
+ * Requirement 7.1: Provides integration with effect-redis connection system
151
+ */
152
+ export const createRedisConnectionLayer = (
153
+ host: string,
154
+ port: number,
155
+ options?: {
156
+ url?: string;
157
+ timeout?: number;
158
+ retryAttempts?: number;
159
+ retryDelay?: number;
160
+ },
161
+ ): Layer.Layer<RedisConnectionOptions> => {
162
+ const redisUrl = options?.url ?? `redis://${host}:${port}`;
163
+ const timeout = options?.timeout ?? 5000;
164
+ const retryAttempts = options?.retryAttempts ?? 3;
165
+ const retryDelay = options?.retryDelay ?? 1000;
166
+
167
+ return RedisConnectionOptionsLive({
168
+ url: redisUrl,
169
+ socket: {
170
+ connectTimeout: timeout,
171
+ reconnectStrategy: (retries: number) => {
172
+ if (retries >= retryAttempts) {
173
+ return false;
174
+ }
175
+ return Math.min(retryDelay * 2 ** retries, 30000);
176
+ },
177
+ },
178
+ commandsQueueMaxLength: 1000,
179
+ });
180
+ };
181
+
182
+ /**
183
+ * Create a layer with custom Redis connection options
184
+ */
185
+ export const createRedisOptionsLayer = (
186
+ options: Parameters<typeof RedisConnectionOptionsLive>[0],
187
+ ): Layer.Layer<RedisConnectionOptions> => RedisConnectionOptionsLive(options);
188
+
189
+ /**
190
+ * Create a test configuration layer
191
+ */
192
+ export const createTestConfigLayer = (
193
+ config: RedistailConfig,
194
+ ): Layer.Layer<RedistailConfig> => Layer.succeed(RedistailConfigTag, config);
195
+
196
+ // ============================================================================
197
+ // Application Layer Composition
198
+ // ============================================================================
199
+
200
+ /**
201
+ * Complete application layer that provides all services needed for redistail
202
+ * This is the main layer that applications should use to get all dependencies
203
+ *
204
+ * OPTIMIZATION: Only includes Redis services that are actually used by the CLI:
205
+ * - RedisPubSub: For PubSub channel monitoring
206
+ * - RedisStream: For Stream monitoring
207
+ * - Core Redis service (RedisLive) is excluded as it's not used by redistail
208
+ *
209
+ * This reduces Redis connections from 5 to 4 (2 for PubSub + 2 for Stream)
210
+ *
211
+ * Requirements 7.1, 7.2, 7.3, 7.4: This layer integrates with effect-redis,
212
+ * uses Effect-TS patterns, provides service composition and proper error handling
213
+ */
214
+
215
+ // Build the base configuration layer first
216
+ // CLIConfigServiceLive has no dependencies
217
+ // RedistailConfigLive depends on CLIConfigService
218
+ const ConfigLayer = Layer.provide(RedistailConfigLive, CLIConfigServiceLive);
219
+
220
+ // RedisConnectionLive depends on RedistailConfig
221
+ // We need to provide RedistailConfig to it, which comes from ConfigLayer
222
+ const ConnectionLayer = Layer.provide(RedisConnectionLive, ConfigLayer);
223
+
224
+ // Only create the Redis services that are actually used by the CLI
225
+ // RedisLive (core Redis service) is not used by redistail CLI
226
+ const RedisPubSubServiceLayer = Layer.provide(RedisPubSubLive, ConnectionLayer);
227
+ const RedisStreamServiceLayer = Layer.provide(RedisStreamLive, ConnectionLayer);
228
+
229
+ // Merge only the Redis services that are actually needed
230
+ const RequiredRedisServicesLayer = Layer.mergeAll(
231
+ RedisPubSubServiceLayer,
232
+ RedisStreamServiceLayer,
233
+ );
234
+
235
+ // DisplayServiceLive depends on RedistailConfig
236
+ const DisplayLayer = Layer.provide(DisplayServiceLive, ConfigLayer);
237
+
238
+ // PubSubMonitorServiceLive depends on RedisPubSub and CLIConfigService
239
+ const PubSubMonitorLayer = Layer.provide(
240
+ PubSubMonitorServiceLive,
241
+ Layer.merge(RedisPubSubServiceLayer, CLIConfigServiceLive),
242
+ );
243
+
244
+ // StreamMonitorServiceLive depends on RedisStream and CLIConfigService
245
+ const StreamMonitorLayer = Layer.provide(
246
+ StreamMonitorServiceLive,
247
+ Layer.merge(RedisStreamServiceLayer, CLIConfigServiceLive),
248
+ );
249
+
250
+ // Compose the final application layer
251
+ export const AppLive = Layer.mergeAll(
252
+ CLIConfigServiceLive,
253
+ SignalHandlerServiceLive,
254
+ ConfigLayer,
255
+ DisplayLayer,
256
+ RequiredRedisServicesLayer,
257
+ PubSubMonitorLayer,
258
+ StreamMonitorLayer,
259
+ );
260
+
261
+ /**
262
+ * Layer for PubSub-only applications
263
+ * Provides only the services needed for PubSub monitoring
264
+ *
265
+ * Requirement 7.3: Service composition through Effect Layers
266
+ */
267
+ export const PubSubAppLive = (() => {
268
+ // Build the dependency chain step by step
269
+ const configLayer = Layer.provide(RedistailConfigLive, CLIConfigServiceLive);
270
+ const connectionLayer = Layer.provide(RedisConnectionLive, configLayer);
271
+ const displayLayer = Layer.provide(DisplayServiceLive, configLayer);
272
+ const pubsubLayer = Layer.provide(RedisPubSubLive, connectionLayer);
273
+ const monitorLayer = Layer.provide(
274
+ PubSubMonitorServiceLive,
275
+ Layer.merge(pubsubLayer, CLIConfigServiceLive),
276
+ );
277
+
278
+ // Merge all the layers together - only what's needed for PubSub
279
+ return Layer.mergeAll(
280
+ CLIConfigServiceLive,
281
+ SignalHandlerServiceLive,
282
+ configLayer,
283
+ displayLayer,
284
+ pubsubLayer,
285
+ monitorLayer,
286
+ );
287
+ })();
288
+
289
+ /**
290
+ * Layer for Stream-only applications
291
+ * Provides only the services needed for Stream monitoring
292
+ *
293
+ * Requirement 7.3: Service composition through Effect Layers
294
+ */
295
+ export const StreamAppLive = (() => {
296
+ // Build the dependency chain step by step
297
+ const configLayer = Layer.provide(RedistailConfigLive, CLIConfigServiceLive);
298
+ const connectionLayer = Layer.provide(RedisConnectionLive, configLayer);
299
+ const displayLayer = Layer.provide(DisplayServiceLive, configLayer);
300
+ const streamLayer = Layer.provide(RedisStreamLive, connectionLayer);
301
+ const monitorLayer = Layer.provide(
302
+ StreamMonitorServiceLive,
303
+ Layer.merge(streamLayer, CLIConfigServiceLive),
304
+ );
305
+
306
+ // Merge all the layers together - only what's needed for Streams
307
+ return Layer.mergeAll(
308
+ CLIConfigServiceLive,
309
+ SignalHandlerServiceLive,
310
+ configLayer,
311
+ displayLayer,
312
+ streamLayer,
313
+ monitorLayer,
314
+ );
315
+ })();
316
+
317
+ // ============================================================================
318
+ // Testing Layers
319
+ // ============================================================================
320
+
321
+ /**
322
+ * Create a test layer with custom configuration
323
+ * Useful for testing with specific Redis configurations
324
+ *
325
+ * Requirement 7.4: Proper resource management for testing
326
+ */
327
+ export const createTestAppLayer = (config: RedistailConfig) =>
328
+ Layer.mergeAll(CLIConfigServiceLive, SignalHandlerServiceLive).pipe(
329
+ Layer.provideMerge(createTestConfigLayer(config)),
330
+ Layer.provideMerge(RedisConnectionLive),
331
+ Layer.provideMerge(
332
+ Layer.mergeAll(RedisLive, RedisPubSubLive, RedisStreamLive),
333
+ ),
334
+ Layer.provideMerge(DisplayServiceLive),
335
+ Layer.provideMerge(
336
+ Layer.mergeAll(PubSubMonitorServiceLive, StreamMonitorServiceLive),
337
+ ),
338
+ );
339
+
340
+ /**
341
+ * Create a minimal test layer with mock Redis connection
342
+ * Useful for unit testing without actual Redis connection
343
+ *
344
+ * Requirement 7.4: Resource management for testing scenarios
345
+ */
346
+ export const createMockAppLayer = (config: RedistailConfig) =>
347
+ Layer.mergeAll(CLIConfigServiceLive, SignalHandlerServiceLive).pipe(
348
+ Layer.provideMerge(createTestConfigLayer(config)),
349
+ Layer.provideMerge(DisplayServiceLive),
350
+ );
351
+
352
+ // ============================================================================
353
+ // Error Handling Utilities
354
+ // ============================================================================
355
+
356
+ /**
357
+ * Helper to handle layer initialization errors gracefully
358
+ * Provides proper error handling and resource management (Requirement 7.4)
359
+ */
360
+ export const handleLayerError = <E>(error: E): Effect.Effect<never, never> =>
361
+ Effect.gen(function* () {
362
+ // Log the error details
363
+ yield* Effect.logError(`Layer initialization failed: ${String(error)}`);
364
+
365
+ // Provide specific error messages based on error type
366
+ if (typeof error === 'object' && error !== null && '_tag' in error) {
367
+ const taggedError = error as { _tag: string; message?: string };
368
+ if (taggedError._tag === 'ConfigError') {
369
+ yield* Effect.logError(
370
+ 'Configuration error - check environment variables and Redis connection settings',
371
+ );
372
+ } else if (taggedError._tag === 'RedisConnectionError') {
373
+ yield* Effect.logError(
374
+ 'Redis connection error - check Redis server availability',
375
+ );
376
+ } else if (taggedError._tag === 'MonitoringError') {
377
+ yield* Effect.logError(
378
+ 'Monitoring error - check Redis connection and topic names',
379
+ );
380
+ }
381
+ }
382
+
383
+ // Exit with error code
384
+ return yield* Effect.sync(() => process.exit(1));
385
+ });
386
+
387
+ /**
388
+ * Create a default RedistailConfig for testing and development
389
+ *
390
+ * Requirement 7.4: Proper configuration management
391
+ */
392
+ export const createDefaultConfig = (
393
+ overrides?: Partial<RedistailConfig>,
394
+ ): RedistailConfig =>
395
+ RedistailConfigConstructor({
396
+ redis: {
397
+ host: '127.0.0.1',
398
+ port: 6379,
399
+ timeout: 5000,
400
+ retryAttempts: 3,
401
+ retryDelay: 1000,
402
+ ...overrides?.redis,
403
+ },
404
+ display: {
405
+ colors: true,
406
+ timestamps: true,
407
+ prettyJson: true,
408
+ ...overrides?.display,
409
+ },
410
+ monitoring: {
411
+ blockTimeout: 5000,
412
+ maxReconnectAttempts: 5,
413
+ ...overrides?.monitoring,
414
+ },
415
+ });
416
+
417
+ // ============================================================================
418
+ // Layer Validation and Utilities
419
+ // ============================================================================
420
+
421
+ /**
422
+ * Validate that all required services are available in a layer
423
+ * This is a compile-time check to ensure layer completeness
424
+ *
425
+ * Requirement 7.3: Service composition validation
426
+ */
427
+ export const validateAppLayer = (): void => {
428
+ // This function serves as a compile-time check for layer dependencies
429
+ // If there are missing dependencies, TypeScript will show errors here
430
+
431
+ // The AppLive layer should provide all these services
432
+ const _appLayerCheck = AppLive;
433
+
434
+ // Suppress unused variable warning
435
+ void _appLayerCheck;
436
+ };
437
+
438
+ // ============================================================================
439
+ // Re-exports for Convenience
440
+ // ============================================================================
441
+
442
+ // Re-export commonly used services and types for convenience
443
+ export {
444
+ CLIConfigService,
445
+ DisplayServiceTag,
446
+ PubSubMonitorService,
447
+ StreamMonitorService,
448
+ SignalHandlerService,
449
+ Redis,
450
+ RedisPubSub,
451
+ RedisStream,
452
+ };
453
+
454
+ export type { RedistailConfig, RedistailError, ConfigError };
455
+
456
+ // Export the RedistailConfigTag for use in other modules
457
+ export { RedistailConfigTag };
458
+
459
+ // ============================================================================
460
+ // Documentation and Examples
461
+ // ============================================================================
462
+
463
+ /**
464
+ * Example usage of the layers:
465
+ *
466
+ * ```typescript
467
+ * import { AppLive, runWithAppLayer } from './layers.js';
468
+ * import { Effect } from 'effect';
469
+ *
470
+ * const myProgram = Effect.gen(function* () {
471
+ * const cliService = yield* CLIConfigService;
472
+ * const config = yield* cliService.loadConfig();
473
+ * // ... use other services
474
+ * });
475
+ *
476
+ * // Run with complete application layer
477
+ * Effect.runPromise(Effect.provide(myProgram, AppLive));
478
+ * ```
479
+ *
480
+ * For testing:
481
+ *
482
+ * ```typescript
483
+ * import { createTestAppLayer, createDefaultConfig } from './layers.js';
484
+ *
485
+ * const testConfig = createDefaultConfig({
486
+ * redis: { host: 'localhost', port: 6380 }
487
+ * });
488
+ *
489
+ * const testLayer = createTestAppLayer(testConfig);
490
+ * Effect.runPromise(Effect.provide(myProgram, testLayer));
491
+ * ```
492
+ */