@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.
- package/dist/cli/redistail.exe +0 -0
- package/dist/cli/redistail.js +1 -1
- package/package.json +3 -2
- 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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display service for formatting and outputting Redis messages.
|
|
3
|
+
*
|
|
4
|
+
* This service handles message formatting for both PubSub and Stream messages,
|
|
5
|
+
* including timestamp formatting, color support, JSON pretty-printing,
|
|
6
|
+
* safe handling of binary content, and proper output stream separation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Context, Effect, Console, Layer } from 'effect';
|
|
10
|
+
import type { PubSubMessage, StreamMessage, RedistailError } from './types.js';
|
|
11
|
+
import { RedistailConfig } from './types.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Service Interface
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Service interface for message display and formatting
|
|
19
|
+
*/
|
|
20
|
+
export interface DisplayService {
|
|
21
|
+
readonly formatPubSubMessage: (
|
|
22
|
+
message: PubSubMessage,
|
|
23
|
+
) => Effect.Effect<string, never>;
|
|
24
|
+
readonly formatStreamMessage: (
|
|
25
|
+
message: StreamMessage,
|
|
26
|
+
) => Effect.Effect<string, never>;
|
|
27
|
+
readonly formatError: (error: RedistailError) => Effect.Effect<string, never>;
|
|
28
|
+
readonly outputMessage: (formatted: string) => Effect.Effect<void, never>;
|
|
29
|
+
readonly outputError: (error: string) => Effect.Effect<void, never>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service tag for dependency injection
|
|
34
|
+
*/
|
|
35
|
+
export const DisplayServiceTag = Context.GenericTag<DisplayService>(
|
|
36
|
+
'@redistail/DisplayService',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Color Constants
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ANSI color codes for terminal output
|
|
45
|
+
*/
|
|
46
|
+
const COLORS = {
|
|
47
|
+
RESET: '\x1b[0m',
|
|
48
|
+
TIMESTAMP: '\x1b[90m', // Bright black (gray)
|
|
49
|
+
PUBSUB_CHANNEL: '\x1b[36m', // Cyan
|
|
50
|
+
STREAM_INFO: '\x1b[35m', // Magenta
|
|
51
|
+
FIELD_KEY: '\x1b[33m', // Yellow
|
|
52
|
+
CONTENT: '\x1b[37m', // White
|
|
53
|
+
ERROR: '\x1b[31m', // Red
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Utility Functions
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format a Date object as an ISO-like timestamp for display
|
|
62
|
+
*/
|
|
63
|
+
const formatTimestamp = (date: Date): string =>
|
|
64
|
+
date.toISOString().replace('T', ' ').replace('Z', '');
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Attempt to pretty-print JSON content if valid, otherwise return as-is
|
|
68
|
+
*/
|
|
69
|
+
const tryPrettyPrintJson = (content: string, prettyJson: boolean): string => {
|
|
70
|
+
if (!prettyJson) return content;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(content);
|
|
74
|
+
return JSON.stringify(parsed, null, 2);
|
|
75
|
+
} catch {
|
|
76
|
+
// Not valid JSON, return original content
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Escape non-printable characters in content for safe display
|
|
83
|
+
*/
|
|
84
|
+
const escapeNonPrintable = (content: string): string => {
|
|
85
|
+
// Create regex for non-printable characters using String.fromCharCode to avoid linter issues
|
|
86
|
+
const chars = [];
|
|
87
|
+
// Add control characters 0x00-0x1F
|
|
88
|
+
for (let i = 0; i <= 0x1f; i++) {
|
|
89
|
+
chars.push(String.fromCharCode(i));
|
|
90
|
+
}
|
|
91
|
+
// Add DEL and extended control characters 0x7F-0x9F
|
|
92
|
+
for (let i = 0x7f; i <= 0x9f; i++) {
|
|
93
|
+
chars.push(String.fromCharCode(i));
|
|
94
|
+
}
|
|
95
|
+
const nonPrintableRegex = new RegExp(`[${chars.join('')}]`, 'g');
|
|
96
|
+
|
|
97
|
+
return content.replace(nonPrintableRegex, (char) => {
|
|
98
|
+
const code = char.charCodeAt(0);
|
|
99
|
+
return `\\x${code.toString(16).padStart(2, '0')}`;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apply color to text if colors are enabled
|
|
105
|
+
*/
|
|
106
|
+
const colorize = (text: string, color: string, enabled: boolean): string =>
|
|
107
|
+
enabled ? `${color}${text}${COLORS.RESET}` : text;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if content appears to be binary (contains null bytes or high percentage of non-printable chars)
|
|
111
|
+
*/
|
|
112
|
+
const isBinaryContent = (content: string): boolean => {
|
|
113
|
+
// Check for null bytes (common in binary data)
|
|
114
|
+
if (content.includes('\0')) return true;
|
|
115
|
+
|
|
116
|
+
// Create regex for non-printable characters using String.fromCharCode to avoid linter issues
|
|
117
|
+
const chars = [];
|
|
118
|
+
// Add control characters 0x00-0x1F
|
|
119
|
+
for (let i = 0; i <= 0x1f; i++) {
|
|
120
|
+
chars.push(String.fromCharCode(i));
|
|
121
|
+
}
|
|
122
|
+
// Add DEL and extended control characters 0x7F-0x9F
|
|
123
|
+
for (let i = 0x7f; i <= 0x9f; i++) {
|
|
124
|
+
chars.push(String.fromCharCode(i));
|
|
125
|
+
}
|
|
126
|
+
const nonPrintableRegex = new RegExp(`[${chars.join('')}]`, 'g');
|
|
127
|
+
|
|
128
|
+
const nonPrintableCount = (content.match(nonPrintableRegex) || []).length;
|
|
129
|
+
const nonPrintableRatio = nonPrintableCount / content.length;
|
|
130
|
+
|
|
131
|
+
// Consider binary if more than 10% non-printable characters
|
|
132
|
+
return nonPrintableRatio > 0.1;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format content safely, handling JSON, binary data, and non-printable characters
|
|
137
|
+
*/
|
|
138
|
+
const formatContent = (
|
|
139
|
+
content: string,
|
|
140
|
+
prettyJson: boolean,
|
|
141
|
+
colors: boolean,
|
|
142
|
+
): string => {
|
|
143
|
+
// First, check if content is binary and needs escaping
|
|
144
|
+
const safeContent = isBinaryContent(content)
|
|
145
|
+
? escapeNonPrintable(content)
|
|
146
|
+
: content;
|
|
147
|
+
|
|
148
|
+
// Try to pretty-print JSON if enabled
|
|
149
|
+
const formattedContent = tryPrettyPrintJson(safeContent, prettyJson);
|
|
150
|
+
|
|
151
|
+
// Apply color if enabled
|
|
152
|
+
return colorize(formattedContent, COLORS.CONTENT, colors);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Service Implementation
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a DisplayService implementation with the given configuration
|
|
161
|
+
*/
|
|
162
|
+
const createDisplayService = (config: RedistailConfig): DisplayService => ({
|
|
163
|
+
formatPubSubMessage: (message: PubSubMessage) =>
|
|
164
|
+
Effect.sync(() => {
|
|
165
|
+
const parts: string[] = [];
|
|
166
|
+
|
|
167
|
+
// Add timestamp if enabled
|
|
168
|
+
if (config.display.timestamps) {
|
|
169
|
+
const timestamp = formatTimestamp(message.timestamp);
|
|
170
|
+
parts.push(
|
|
171
|
+
colorize(timestamp, COLORS.TIMESTAMP, config.display.colors),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Add channel name in brackets
|
|
176
|
+
const channel = `[${message.channel}]`;
|
|
177
|
+
parts.push(
|
|
178
|
+
colorize(channel, COLORS.PUBSUB_CHANNEL, config.display.colors),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Add formatted content
|
|
182
|
+
const content = formatContent(
|
|
183
|
+
message.content,
|
|
184
|
+
config.display.prettyJson,
|
|
185
|
+
config.display.colors,
|
|
186
|
+
);
|
|
187
|
+
parts.push(content);
|
|
188
|
+
|
|
189
|
+
return parts.join(' ');
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
formatStreamMessage: (message: StreamMessage) =>
|
|
193
|
+
Effect.sync(() => {
|
|
194
|
+
const parts: string[] = [];
|
|
195
|
+
|
|
196
|
+
// Add timestamp if enabled
|
|
197
|
+
if (config.display.timestamps) {
|
|
198
|
+
const timestamp = formatTimestamp(message.timestamp);
|
|
199
|
+
parts.push(
|
|
200
|
+
colorize(timestamp, COLORS.TIMESTAMP, config.display.colors),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Add stream key and entry ID in brackets
|
|
205
|
+
const streamInfo = `[${message.streamKey}:${message.entryId}]`;
|
|
206
|
+
parts.push(
|
|
207
|
+
colorize(streamInfo, COLORS.STREAM_INFO, config.display.colors),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Format fields as key=value pairs
|
|
211
|
+
const fieldPairs = Object.entries(message.fields).map(([key, value]) => {
|
|
212
|
+
const coloredKey = colorize(
|
|
213
|
+
key,
|
|
214
|
+
COLORS.FIELD_KEY,
|
|
215
|
+
config.display.colors,
|
|
216
|
+
);
|
|
217
|
+
const formattedValue = formatContent(
|
|
218
|
+
value,
|
|
219
|
+
config.display.prettyJson,
|
|
220
|
+
config.display.colors,
|
|
221
|
+
);
|
|
222
|
+
return `${coloredKey}=${formattedValue}`;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (fieldPairs.length > 0) {
|
|
226
|
+
parts.push(fieldPairs.join(' '));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return parts.join(' ');
|
|
230
|
+
}),
|
|
231
|
+
|
|
232
|
+
formatError: (error: RedistailError) =>
|
|
233
|
+
Effect.sync(() => {
|
|
234
|
+
const errorMessage = `ERROR: ${error.message}`;
|
|
235
|
+
return colorize(errorMessage, COLORS.ERROR, config.display.colors);
|
|
236
|
+
}),
|
|
237
|
+
|
|
238
|
+
outputMessage: (formatted: string) => Console.log(formatted), // Messages go to stdout
|
|
239
|
+
|
|
240
|
+
outputError: (error: string) => Console.error(error), // Errors go to stderr
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Service Layer
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create a DisplayService layer with the given configuration
|
|
249
|
+
*/
|
|
250
|
+
export const makeDisplayServiceLayer = (
|
|
251
|
+
config: RedistailConfig,
|
|
252
|
+
): Layer.Layer<DisplayService> =>
|
|
253
|
+
Layer.succeed(DisplayServiceTag, createDisplayService(config));
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Live layer that provides DisplayService implementation
|
|
257
|
+
* This is a convenience layer that uses default configuration
|
|
258
|
+
*/
|
|
259
|
+
export const DisplayServiceLive: Layer.Layer<DisplayService> = Layer.effect(
|
|
260
|
+
DisplayServiceTag,
|
|
261
|
+
Effect.sync(() => {
|
|
262
|
+
// Use default configuration for the live layer
|
|
263
|
+
// In practice, this would be provided by the application layer
|
|
264
|
+
const defaultConfig = RedistailConfig({
|
|
265
|
+
redis: {
|
|
266
|
+
host: '127.0.0.1',
|
|
267
|
+
port: 6379,
|
|
268
|
+
timeout: 5000,
|
|
269
|
+
retryAttempts: 3,
|
|
270
|
+
retryDelay: 1000,
|
|
271
|
+
},
|
|
272
|
+
display: {
|
|
273
|
+
colors: true,
|
|
274
|
+
timestamps: true,
|
|
275
|
+
prettyJson: true,
|
|
276
|
+
},
|
|
277
|
+
monitoring: {
|
|
278
|
+
blockTimeout: 5000,
|
|
279
|
+
maxReconnectAttempts: 5,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
return createDisplayService(defaultConfig);
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Utility Functions for Testing
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a DisplayService for testing with custom configuration
|
|
292
|
+
*/
|
|
293
|
+
export const createTestDisplayService = (
|
|
294
|
+
config: RedistailConfig,
|
|
295
|
+
): DisplayService => createDisplayService(config);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Helper to strip ANSI color codes from formatted output for testing
|
|
299
|
+
*/
|
|
300
|
+
export const stripAnsiColors = (text: string): string => {
|
|
301
|
+
// Create regex for ANSI escape sequences using String.fromCharCode to avoid linter issues
|
|
302
|
+
const esc = String.fromCharCode(0x1b); // ESC character
|
|
303
|
+
const ansiRegex = new RegExp(`${esc}\\[[0-9;]*m`, 'g');
|
|
304
|
+
return text.replace(ansiRegex, '');
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Helper to check if text contains ANSI color codes
|
|
309
|
+
*/
|
|
310
|
+
export const hasAnsiColors = (text: string): boolean => {
|
|
311
|
+
// Create regex for ANSI escape sequences using String.fromCharCode to avoid linter issues
|
|
312
|
+
const esc = String.fromCharCode(0x1b); // ESC character
|
|
313
|
+
const ansiRegex = new RegExp(`${esc}\\[[0-9;]*m`);
|
|
314
|
+
return ansiRegex.test(text);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// ============================================================================
|
|
318
|
+
// Additional Exports
|
|
319
|
+
// ============================================================================
|
|
320
|
+
|
|
321
|
+
// All exports are already defined above
|