@ceschiatti/redistail 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 +680 -0
- package/dist/cli/redistail.exe +0 -0
- package/dist/cli/redistail.js +5 -5
- package/package.json +38 -37
- package/src/cli/config-service.ts +455 -0
- package/src/cli/display-service.ts +321 -0
- package/src/cli/layers.ts +492 -0
- package/src/cli/pubsub-monitor-service.ts +296 -0
- package/src/cli/redistail-launcher.js +65 -0
- package/src/cli/redistail.ts +417 -0
- package/src/cli/signal-handler-service.ts +210 -0
- package/src/cli/stream-monitor-service.ts +427 -0
- package/src/cli/types.ts +302 -0
- package/src/cli/version-generated.ts +27 -0
- package/src/cli/version.ts +27 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main CLI program for redistail - Redis message monitoring utility.
|
|
3
|
+
*
|
|
4
|
+
* This module implements the main program effect that combines all services
|
|
5
|
+
* to create a complete Redis monitoring application. It handles CLI argument
|
|
6
|
+
* parsing, service initialization, signal handling, and message streaming.
|
|
7
|
+
*
|
|
8
|
+
* Requirements addressed:
|
|
9
|
+
* - 1.5: Help and version flag support
|
|
10
|
+
* - 2.5: Graceful shutdown for PubSub monitoring (Ctrl+C handling)
|
|
11
|
+
* - 3.5: Graceful shutdown for Stream monitoring (Ctrl+C handling)
|
|
12
|
+
* - 6.3: Topic validation and error handling
|
|
13
|
+
* - 6.4: Redis error recovery and continued monitoring
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { BunRuntime } from '@effect/platform-bun';
|
|
17
|
+
import { Console, Duration, Effect, pipe, Schedule, Stream } from 'effect';
|
|
18
|
+
// Note: RedisPubSub and RedisStream are provided transitively via PubSubMonitorService and StreamMonitorService
|
|
19
|
+
import { CLIConfigService, CLIConfigServiceLive } from './config-service.js';
|
|
20
|
+
import { DisplayServiceTag, type DisplayService } from './display-service.js';
|
|
21
|
+
import { PubSubAppLive, StreamAppLive } from './layers.js';
|
|
22
|
+
import { PubSubMonitorService } from './pubsub-monitor-service.js';
|
|
23
|
+
import { SignalHandlerService } from './signal-handler-service.js';
|
|
24
|
+
import { StreamMonitorService } from './stream-monitor-service.js';
|
|
25
|
+
import type { CLIConfig, RedistailError } from './types.js';
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Main Program Implementation
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Main redistail program effect that combines all services
|
|
33
|
+
*
|
|
34
|
+
* This effect:
|
|
35
|
+
* 1. Parses CLI arguments and loads configuration
|
|
36
|
+
* 2. Handles help and version flags
|
|
37
|
+
* 3. Sets up signal handlers for graceful shutdown
|
|
38
|
+
* 4. Starts appropriate monitoring based on connection type
|
|
39
|
+
* 5. Streams and displays messages until interrupted
|
|
40
|
+
*
|
|
41
|
+
* Requirements 1.5, 2.5, 3.5, 6.3, 6.4
|
|
42
|
+
*/
|
|
43
|
+
export const redistailProgram = (
|
|
44
|
+
args: string[],
|
|
45
|
+
): Effect.Effect<
|
|
46
|
+
void,
|
|
47
|
+
RedistailError,
|
|
48
|
+
| CLIConfigService
|
|
49
|
+
| SignalHandlerService
|
|
50
|
+
| DisplayService
|
|
51
|
+
| PubSubMonitorService
|
|
52
|
+
| StreamMonitorService
|
|
53
|
+
> =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
// Parse CLI arguments
|
|
56
|
+
const cliConfigService = yield* CLIConfigService;
|
|
57
|
+
const cliConfig = yield* cliConfigService.parseArgs(args);
|
|
58
|
+
|
|
59
|
+
// Handle help and version flags (Requirement 1.5)
|
|
60
|
+
if (cliConfig.help) {
|
|
61
|
+
yield* cliConfigService.showHelp();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cliConfig.version) {
|
|
66
|
+
yield* cliConfigService.showVersion();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Setup signal handlers for graceful shutdown (Requirements 2.5, 3.5)
|
|
71
|
+
const signalHandler = yield* SignalHandlerService;
|
|
72
|
+
yield* signalHandler.setupSignalHandlers();
|
|
73
|
+
|
|
74
|
+
// Get required services
|
|
75
|
+
const displayService = yield* DisplayServiceTag;
|
|
76
|
+
|
|
77
|
+
// Start monitoring based on connection type
|
|
78
|
+
yield* startMonitoring(cliConfig, displayService);
|
|
79
|
+
}) as Effect.Effect<
|
|
80
|
+
void,
|
|
81
|
+
RedistailError,
|
|
82
|
+
| CLIConfigService
|
|
83
|
+
| SignalHandlerService
|
|
84
|
+
| DisplayService
|
|
85
|
+
| PubSubMonitorService
|
|
86
|
+
| StreamMonitorService
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start monitoring based on the connection type specified in CLI config
|
|
91
|
+
*
|
|
92
|
+
* This function handles both PubSub and Stream monitoring with proper
|
|
93
|
+
* error handling and recovery (Requirements 6.3, 6.4)
|
|
94
|
+
*/
|
|
95
|
+
const startMonitoring = (
|
|
96
|
+
cliConfig: CLIConfig,
|
|
97
|
+
displayService: DisplayService,
|
|
98
|
+
): Effect.Effect<
|
|
99
|
+
void,
|
|
100
|
+
RedistailError,
|
|
101
|
+
PubSubMonitorService | StreamMonitorService | CLIConfigService
|
|
102
|
+
> =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
// Validate topic name (Requirement 6.3)
|
|
105
|
+
if (!cliConfig.topicName || cliConfig.topicName.trim().length === 0) {
|
|
106
|
+
yield* displayService.outputError('ā Error: Topic name cannot be empty');
|
|
107
|
+
return yield* Effect.fail(
|
|
108
|
+
new Error('Invalid topic name') as RedistailError,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const topicName = cliConfig.topicName.trim();
|
|
113
|
+
|
|
114
|
+
// Log connection attempt to stderr
|
|
115
|
+
yield* displayService.outputError(
|
|
116
|
+
`š Connecting to Redis ${cliConfig.connectionType} monitoring for topic: ${topicName}`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (cliConfig.connectionType === 'pubsub') {
|
|
120
|
+
yield* startPubSubMonitoring(topicName, displayService);
|
|
121
|
+
} else {
|
|
122
|
+
yield* startStreamMonitoring(topicName, displayService);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start PubSub monitoring with error recovery
|
|
128
|
+
*
|
|
129
|
+
* Requirements 2.5, 6.4: Graceful shutdown and Redis error recovery
|
|
130
|
+
*/
|
|
131
|
+
const startPubSubMonitoring = (
|
|
132
|
+
channel: string,
|
|
133
|
+
displayService: DisplayService,
|
|
134
|
+
): Effect.Effect<void, RedistailError, PubSubMonitorService> =>
|
|
135
|
+
Effect.gen(function* () {
|
|
136
|
+
const pubSubMonitor = yield* PubSubMonitorService;
|
|
137
|
+
|
|
138
|
+
// Log successful connection
|
|
139
|
+
yield* displayService.outputError(
|
|
140
|
+
`ā
Connected to PubSub channel: ${channel}`,
|
|
141
|
+
);
|
|
142
|
+
yield* displayService.outputError(
|
|
143
|
+
'š” Waiting for messages... (Press Ctrl+C to exit)',
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Create message stream with error recovery (Requirement 6.4)
|
|
147
|
+
const messageStream = pubSubMonitor.monitor(channel).pipe(
|
|
148
|
+
Stream.mapEffect((message) =>
|
|
149
|
+
displayService.formatPubSubMessage(message),
|
|
150
|
+
),
|
|
151
|
+
Stream.mapEffect((formatted) => displayService.outputMessage(formatted)),
|
|
152
|
+
// Retry on recoverable errors with exponential backoff
|
|
153
|
+
Stream.retry(
|
|
154
|
+
pipe(
|
|
155
|
+
Schedule.exponential(Duration.seconds(1)),
|
|
156
|
+
Schedule.intersect(Schedule.recurs(5)), // Max 5 retry attempts
|
|
157
|
+
Schedule.tapInput(
|
|
158
|
+
(error: unknown) =>
|
|
159
|
+
displayService.outputError(
|
|
160
|
+
`ā ļø Connection error, retrying... ${String(error)}`,
|
|
161
|
+
) as Effect.Effect<void, never, never>,
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
// Handle non-recoverable errors gracefully
|
|
166
|
+
Stream.catchAll((error: unknown) =>
|
|
167
|
+
Stream.fromEffect(
|
|
168
|
+
pipe(
|
|
169
|
+
Effect.all([
|
|
170
|
+
displayService.outputError(
|
|
171
|
+
`ā Fatal error in PubSub monitoring: ${String(error)}`,
|
|
172
|
+
),
|
|
173
|
+
displayService.outputError(
|
|
174
|
+
'š” Check Redis connection and try again',
|
|
175
|
+
),
|
|
176
|
+
]),
|
|
177
|
+
Effect.asVoid,
|
|
178
|
+
) as Effect.Effect<void, never, never>,
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
) as Stream.Stream<void, never, never>;
|
|
182
|
+
|
|
183
|
+
// Run the message stream until completion or interruption
|
|
184
|
+
yield* Stream.runDrain(messageStream);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Start Stream monitoring with error recovery
|
|
189
|
+
*
|
|
190
|
+
* Requirements 3.5, 6.4: Graceful shutdown and Redis error recovery
|
|
191
|
+
*/
|
|
192
|
+
const startStreamMonitoring = (
|
|
193
|
+
streamKey: string,
|
|
194
|
+
displayService: DisplayService,
|
|
195
|
+
): Effect.Effect<void, RedistailError, StreamMonitorService> =>
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const streamMonitor = yield* StreamMonitorService;
|
|
198
|
+
|
|
199
|
+
// Log successful connection
|
|
200
|
+
yield* displayService.outputError(`ā
Connected to Stream: ${streamKey}`);
|
|
201
|
+
yield* displayService.outputError(
|
|
202
|
+
'š” Waiting for stream entries... (Press Ctrl+C to exit)',
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Create message stream with error recovery (Requirement 6.4)
|
|
206
|
+
const messageStream = streamMonitor.monitor(streamKey).pipe(
|
|
207
|
+
Stream.mapEffect((message) =>
|
|
208
|
+
displayService.formatStreamMessage(message),
|
|
209
|
+
),
|
|
210
|
+
Stream.mapEffect((formatted) => displayService.outputMessage(formatted)),
|
|
211
|
+
// Retry on recoverable errors with exponential backoff
|
|
212
|
+
Stream.retry(
|
|
213
|
+
pipe(
|
|
214
|
+
Schedule.exponential(Duration.seconds(1)),
|
|
215
|
+
Schedule.intersect(Schedule.recurs(5)), // Max 5 retry attempts
|
|
216
|
+
Schedule.tapInput(
|
|
217
|
+
(error: unknown) =>
|
|
218
|
+
displayService.outputError(
|
|
219
|
+
`ā ļø Connection error, retrying... ${String(error)}`,
|
|
220
|
+
) as Effect.Effect<void, never, never>,
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
// Handle non-recoverable errors gracefully
|
|
225
|
+
Stream.catchAll((error: unknown) =>
|
|
226
|
+
Stream.fromEffect(
|
|
227
|
+
pipe(
|
|
228
|
+
Effect.all([
|
|
229
|
+
displayService.outputError(
|
|
230
|
+
`ā Fatal error in Stream monitoring: ${String(error)}`,
|
|
231
|
+
),
|
|
232
|
+
displayService.outputError(
|
|
233
|
+
'š” Check Redis connection and stream name, then try again',
|
|
234
|
+
),
|
|
235
|
+
]),
|
|
236
|
+
Effect.asVoid,
|
|
237
|
+
) as Effect.Effect<void, never, never>,
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
) as Stream.Stream<void, never, never>;
|
|
241
|
+
|
|
242
|
+
// Run the message stream until completion or interruption
|
|
243
|
+
yield* Stream.runDrain(messageStream);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// CLI Entry Point with Error Handling
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* CLI main function with comprehensive error handling and optimized layer selection
|
|
252
|
+
*
|
|
253
|
+
* This function provides the main entry point for the CLI binary with
|
|
254
|
+
* proper error handling, logging, and process management. It optimizes
|
|
255
|
+
* Redis connections by only loading the services needed for the specific mode:
|
|
256
|
+
*
|
|
257
|
+
* - PubSub mode: Only creates 2 Redis connections (publish + subscribe)
|
|
258
|
+
* - Stream mode: Only creates 2 Redis connections (producer + consumer)
|
|
259
|
+
* - Help/version: Uses minimal layer without Redis connections
|
|
260
|
+
* - Invalid args: Shows error without creating any connections
|
|
261
|
+
*
|
|
262
|
+
* This reduces Redis connections from 5 to just 2 per run, or 0 for help/errors.
|
|
263
|
+
*/
|
|
264
|
+
export const cliMain = (args: string[]): Effect.Effect<void, never, never> => {
|
|
265
|
+
// Early validation - check for help/version flags or missing arguments
|
|
266
|
+
// This prevents unnecessary Redis connections for simple cases
|
|
267
|
+
const hasHelpFlag = args.includes('--help') || args.includes('-h');
|
|
268
|
+
const hasVersionFlag = args.includes('--version') || args.includes('-v');
|
|
269
|
+
const hasMissingArgs = args.length < 2 && !hasHelpFlag && !hasVersionFlag;
|
|
270
|
+
|
|
271
|
+
// Handle help/version/error cases without Redis connections
|
|
272
|
+
if (hasHelpFlag || hasVersionFlag || hasMissingArgs) {
|
|
273
|
+
const simpleProgram = Effect.gen(function* () {
|
|
274
|
+
const cliConfigService = yield* CLIConfigService;
|
|
275
|
+
|
|
276
|
+
if (hasHelpFlag) {
|
|
277
|
+
yield* cliConfigService.showHelp();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (hasVersionFlag) {
|
|
282
|
+
yield* cliConfigService.showVersion();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (hasMissingArgs) {
|
|
287
|
+
yield* Console.error(
|
|
288
|
+
'ā CLI Error: Missing required arguments. Usage: redistail <pubsub|stream> <topic-name>',
|
|
289
|
+
);
|
|
290
|
+
yield* Console.error('š” Use --help for usage information');
|
|
291
|
+
return yield* Effect.sync(() => process.exit(1));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Use minimal layer without Redis connections for these cases
|
|
296
|
+
return Effect.provide(simpleProgram, CLIConfigServiceLive) as Effect.Effect<
|
|
297
|
+
void,
|
|
298
|
+
never,
|
|
299
|
+
never
|
|
300
|
+
>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// For actual monitoring, determine connection type and use appropriate layer
|
|
304
|
+
const connectionType = args[0];
|
|
305
|
+
|
|
306
|
+
// Create the program with error handling
|
|
307
|
+
const programWithErrorHandling = pipe(
|
|
308
|
+
redistailProgram(args),
|
|
309
|
+
// Handle all possible errors gracefully
|
|
310
|
+
Effect.catchAll((error: unknown) =>
|
|
311
|
+
Effect.gen(function* () {
|
|
312
|
+
// Determine error type and provide appropriate message
|
|
313
|
+
if (typeof error === 'object' && error !== null && '_tag' in error) {
|
|
314
|
+
const taggedError = error as { _tag: string; message?: string };
|
|
315
|
+
|
|
316
|
+
switch (taggedError._tag) {
|
|
317
|
+
case 'CLIError':
|
|
318
|
+
yield* Console.error(
|
|
319
|
+
`ā CLI Error: ${taggedError.message || String(error)}`,
|
|
320
|
+
);
|
|
321
|
+
yield* Console.error('š” Use --help for usage information');
|
|
322
|
+
break;
|
|
323
|
+
case 'ConfigError':
|
|
324
|
+
yield* Console.error(
|
|
325
|
+
`ā Configuration Error: ${taggedError.message || String(error)}`,
|
|
326
|
+
);
|
|
327
|
+
yield* Console.error(
|
|
328
|
+
'š” Check environment variables and Redis connection settings',
|
|
329
|
+
);
|
|
330
|
+
break;
|
|
331
|
+
case 'MonitoringError':
|
|
332
|
+
yield* Console.error(
|
|
333
|
+
`ā Monitoring Error: ${taggedError.message || String(error)}`,
|
|
334
|
+
);
|
|
335
|
+
yield* Console.error(
|
|
336
|
+
'š” Check Redis server availability and topic names',
|
|
337
|
+
);
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
yield* Console.error(`ā Unexpected Error: ${String(error)}`);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
yield* Console.error(`ā Error: ${String(error)}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Exit with error code
|
|
347
|
+
return yield* Effect.sync(() => process.exit(1));
|
|
348
|
+
}),
|
|
349
|
+
),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Select and provide the appropriate layer based on connection type
|
|
353
|
+
if (connectionType === 'pubsub') {
|
|
354
|
+
return Effect.provide(
|
|
355
|
+
programWithErrorHandling,
|
|
356
|
+
PubSubAppLive,
|
|
357
|
+
) as Effect.Effect<void, never, never>;
|
|
358
|
+
}
|
|
359
|
+
if (connectionType === 'stream') {
|
|
360
|
+
return Effect.provide(
|
|
361
|
+
programWithErrorHandling,
|
|
362
|
+
StreamAppLive,
|
|
363
|
+
) as Effect.Effect<void, never, never>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Invalid connection type - show error without Redis connections
|
|
367
|
+
const errorProgram = Effect.gen(function* () {
|
|
368
|
+
yield* Console.error(
|
|
369
|
+
`ā CLI Error: Invalid connection type '${connectionType}'. Must be 'pubsub' or 'stream'`,
|
|
370
|
+
);
|
|
371
|
+
yield* Console.error('š” Use --help for usage information');
|
|
372
|
+
return yield* Effect.sync(() => process.exit(1));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return Effect.provide(errorProgram, CLIConfigServiceLive) as Effect.Effect<
|
|
376
|
+
void,
|
|
377
|
+
never,
|
|
378
|
+
never
|
|
379
|
+
>;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// Binary Entry Point
|
|
384
|
+
// ============================================================================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Entry point for the CLI binary
|
|
388
|
+
*
|
|
389
|
+
* This is the main entry point when redistail is executed as a CLI command.
|
|
390
|
+
* It processes command line arguments and runs the main program.
|
|
391
|
+
*/
|
|
392
|
+
if (import.meta.main) {
|
|
393
|
+
// Get command line arguments (skip node and script name)
|
|
394
|
+
const args = process.argv.slice(2);
|
|
395
|
+
|
|
396
|
+
// Run the main program with Bun runtime
|
|
397
|
+
BunRuntime.runMain(cliMain(args));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// Library Exports
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Re-export commonly used types and services for convenience
|
|
406
|
+
*/
|
|
407
|
+
export {
|
|
408
|
+
AppLive,
|
|
409
|
+
CLIConfigService,
|
|
410
|
+
DisplayServiceTag,
|
|
411
|
+
PubSubAppLive,
|
|
412
|
+
PubSubMonitorService,
|
|
413
|
+
SignalHandlerService,
|
|
414
|
+
StreamAppLive,
|
|
415
|
+
StreamMonitorService,
|
|
416
|
+
} from './layers.js';
|
|
417
|
+
export type { CLIConfig, RedistailConfig, RedistailError } from './types.js';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal handler service for graceful shutdown of the redistail CLI utility.
|
|
3
|
+
*
|
|
4
|
+
* This module provides signal handling for SIGINT (Ctrl+C) and SIGTERM signals,
|
|
5
|
+
* ensuring proper cleanup and graceful shutdown for both PubSub and Stream monitoring.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Context, Effect, Layer, Console } from 'effect';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Service Interface
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service interface for handling process signals and graceful shutdown
|
|
16
|
+
*/
|
|
17
|
+
export interface SignalHandlerService {
|
|
18
|
+
/**
|
|
19
|
+
* Set up signal handlers for SIGINT and SIGTERM
|
|
20
|
+
*/
|
|
21
|
+
readonly setupSignalHandlers: () => Effect.Effect<void>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Perform graceful shutdown with cleanup
|
|
25
|
+
*/
|
|
26
|
+
readonly gracefulShutdown: () => Effect.Effect<void>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if shutdown has been requested
|
|
30
|
+
*/
|
|
31
|
+
readonly isShutdownRequested: () => Effect.Effect<boolean>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Service tag for dependency injection
|
|
36
|
+
*/
|
|
37
|
+
export const SignalHandlerService = Context.GenericTag<SignalHandlerService>(
|
|
38
|
+
'SignalHandlerService',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Service Implementation
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Internal state to track shutdown requests
|
|
47
|
+
*/
|
|
48
|
+
interface SignalHandlerState {
|
|
49
|
+
shutdownInProgress: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create the signal handler service implementation
|
|
54
|
+
*/
|
|
55
|
+
const createSignalHandler = (): SignalHandlerService => {
|
|
56
|
+
// Mutable state for tracking shutdown status
|
|
57
|
+
const state: SignalHandlerState = {
|
|
58
|
+
shutdownInProgress: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Perform graceful shutdown with cleanup
|
|
63
|
+
*/
|
|
64
|
+
const gracefulShutdown = (): Effect.Effect<void> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
// Prevent multiple shutdown attempts
|
|
67
|
+
if (state.shutdownInProgress) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
state.shutdownInProgress = true;
|
|
72
|
+
|
|
73
|
+
// Log shutdown message
|
|
74
|
+
yield* Console.error('š Shutting down redistail...');
|
|
75
|
+
|
|
76
|
+
// Give a brief moment for any pending operations to complete
|
|
77
|
+
yield* Effect.sleep('100 millis');
|
|
78
|
+
|
|
79
|
+
// Log completion and exit
|
|
80
|
+
yield* Console.error('ā
Shutdown complete');
|
|
81
|
+
return yield* Effect.sync(() => process.exit(0));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle shutdown signal with proper cleanup
|
|
86
|
+
*/
|
|
87
|
+
const handleShutdownSignal = (signal: string): Effect.Effect<void> =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
// Prevent multiple shutdown attempts
|
|
90
|
+
if (state.shutdownInProgress) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
state.shutdownInProgress = true;
|
|
95
|
+
|
|
96
|
+
// Log shutdown message to stderr
|
|
97
|
+
yield* Console.error(
|
|
98
|
+
`\nš Received ${signal}, shutting down gracefully...`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Perform graceful shutdown
|
|
102
|
+
yield* gracefulShutdown();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set up signal handlers for SIGINT and SIGTERM
|
|
107
|
+
*/
|
|
108
|
+
const setupSignalHandlers = (): Effect.Effect<void> =>
|
|
109
|
+
Effect.sync(() => {
|
|
110
|
+
// Handle SIGINT (Ctrl+C)
|
|
111
|
+
process.on('SIGINT', () => {
|
|
112
|
+
Effect.runSync(handleShutdownSignal('SIGINT'));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Handle SIGTERM (termination request)
|
|
116
|
+
process.on('SIGTERM', () => {
|
|
117
|
+
Effect.runSync(handleShutdownSignal('SIGTERM'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Handle uncaught exceptions gracefully
|
|
121
|
+
process.on('uncaughtException', (error) => {
|
|
122
|
+
Effect.runSync(
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
yield* Console.error(`ā Uncaught exception: ${error.message}`);
|
|
125
|
+
yield* gracefulShutdown();
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle unhandled promise rejections
|
|
131
|
+
process.on('unhandledRejection', (reason) => {
|
|
132
|
+
Effect.runSync(
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
yield* Console.error(`ā Unhandled rejection: ${reason}`);
|
|
135
|
+
yield* gracefulShutdown();
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if shutdown has been requested
|
|
143
|
+
*/
|
|
144
|
+
const isShutdownRequested = (): Effect.Effect<boolean> =>
|
|
145
|
+
Effect.sync(() => state.shutdownInProgress);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
setupSignalHandlers,
|
|
149
|
+
gracefulShutdown,
|
|
150
|
+
isShutdownRequested,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Service Layer
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Live implementation layer for the SignalHandlerService
|
|
160
|
+
*/
|
|
161
|
+
export const SignalHandlerServiceLive = Layer.succeed(
|
|
162
|
+
SignalHandlerService,
|
|
163
|
+
createSignalHandler(),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Utility Functions
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Effect that sets up signal handlers and runs an effect
|
|
172
|
+
*/
|
|
173
|
+
export const withSignalHandling = <A, E, R>(
|
|
174
|
+
effect: Effect.Effect<A, E, R>,
|
|
175
|
+
): Effect.Effect<A, E, R | SignalHandlerService> =>
|
|
176
|
+
Effect.gen(function* () {
|
|
177
|
+
const signalHandler = yield* SignalHandlerService;
|
|
178
|
+
|
|
179
|
+
// Set up signal handlers
|
|
180
|
+
yield* signalHandler.setupSignalHandlers();
|
|
181
|
+
|
|
182
|
+
// Run the effect
|
|
183
|
+
return yield* effect;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if shutdown has been requested
|
|
188
|
+
*/
|
|
189
|
+
export const checkShutdown = (): Effect.Effect<
|
|
190
|
+
boolean,
|
|
191
|
+
never,
|
|
192
|
+
SignalHandlerService
|
|
193
|
+
> =>
|
|
194
|
+
Effect.gen(function* () {
|
|
195
|
+
const signalHandler = yield* SignalHandlerService;
|
|
196
|
+
return yield* signalHandler.isShutdownRequested();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Perform graceful shutdown
|
|
201
|
+
*/
|
|
202
|
+
export const performShutdown = (): Effect.Effect<
|
|
203
|
+
void,
|
|
204
|
+
never,
|
|
205
|
+
SignalHandlerService
|
|
206
|
+
> =>
|
|
207
|
+
Effect.gen(function* () {
|
|
208
|
+
const signalHandler = yield* SignalHandlerService;
|
|
209
|
+
yield* signalHandler.gracefulShutdown();
|
|
210
|
+
});
|