service-keepalive 1.0.0

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,375 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ /**
4
+ * Supported HTTP methods for keep-alive pings.
5
+ */
6
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
7
+ /**
8
+ * Strategy for calculating delays between retry attempts.
9
+ */
10
+ type RetryStrategy = 'exponential' | 'linear' | 'fixed';
11
+ /**
12
+ * Logging verbosity levels.
13
+ */
14
+ type LogLevel = 'quiet' | 'normal' | 'verbose';
15
+ /**
16
+ * Custom logger interface.
17
+ */
18
+ interface LoggerInterface {
19
+ info(message: string, ...args: unknown[]): void;
20
+ success(message: string, ...args: unknown[]): void;
21
+ warn(message: string, ...args: unknown[]): void;
22
+ error(message: string, ...args: unknown[]): void;
23
+ debug(message: string, ...args: unknown[]): void;
24
+ }
25
+ /**
26
+ * Result of an individual HTTP ping execution.
27
+ */
28
+ interface PingResult {
29
+ /** Identifier of the service pinged */
30
+ serviceName: string;
31
+ /** Target URL */
32
+ url: string;
33
+ /** HTTP method used */
34
+ method: HttpMethod;
35
+ /** HTTP response status code (0 if network error / timeout) */
36
+ status: number;
37
+ /** HTTP response status text */
38
+ statusText: string;
39
+ /** Whether the ping was deemed successful */
40
+ ok: boolean;
41
+ /** Round-trip duration in milliseconds */
42
+ durationMs: number;
43
+ /** Which attempt number succeeded or failed (1-indexed) */
44
+ attempt: number;
45
+ /** ISO timestamp when the ping was executed */
46
+ timestamp: Date;
47
+ /** Error message if request failed */
48
+ error?: string;
49
+ /** Sanitized response headers if available and requested */
50
+ headers?: Record<string, string>;
51
+ }
52
+ /**
53
+ * Information passed during a retry event.
54
+ */
55
+ interface RetryInfo {
56
+ serviceName: string;
57
+ url: string;
58
+ attempt: number;
59
+ maxRetries: number;
60
+ delayMs: number;
61
+ error: string;
62
+ }
63
+ /**
64
+ * Configuration options for an individual KeepAlive target.
65
+ */
66
+ interface KeepAliveConfig {
67
+ /** Target service URL to ping (must be a valid http or https URL) */
68
+ url: string;
69
+ /** Optional friendly name for the service (defaults to URL hostname) */
70
+ name?: string;
71
+ /** Interval between consecutive pings (e.g. '10m', '30s', '1h', or number in ms). Default: '10m' */
72
+ interval?: string | number;
73
+ /** Request timeout duration (e.g. '30s', '10s', or number in ms). Default: '30s' */
74
+ timeout?: string | number;
75
+ /** HTTP method to use. Default: 'GET' */
76
+ method?: HttpMethod;
77
+ /** Custom request headers */
78
+ headers?: Record<string, string>;
79
+ /** Optional request body payload (for POST/PUT requests) */
80
+ body?: string | null;
81
+ /** Number of retry attempts on failure. Default: 3 */
82
+ retries?: number;
83
+ /** Base delay between retries (e.g. '5s', '1000ms'). Default: '5s' */
84
+ retryDelay?: string | number;
85
+ /** Backoff strategy for retries. Default: 'exponential' */
86
+ retryStrategy?: RetryStrategy;
87
+ /** Whether to add random jitter to retry delays. Default: true */
88
+ retryJitter?: boolean;
89
+ /** Maximum upper bound for retry delay. Default: '60s' */
90
+ maxRetryDelay?: string | number;
91
+ /**
92
+ * Status code(s) considered successful.
93
+ * Can be an array of status codes or a predicate function.
94
+ * Default: any 2xx status (200-299)
95
+ */
96
+ expectedStatusCodes?: number[] | ((status: number) => boolean);
97
+ /** Custom logger or false to disable logging. Default: built-in console logger */
98
+ logger?: LoggerInterface | false;
99
+ /** Output verbosity level. Default: 'normal' */
100
+ logLevel?: LogLevel;
101
+ /** Whether the underlying timer should not prevent the Node process from exiting. Default: false */
102
+ unrefTimer?: boolean;
103
+ }
104
+ /**
105
+ * Configuration options for managing multiple services.
106
+ */
107
+ interface MultiKeepAliveConfig {
108
+ /** List of service configurations to keep alive */
109
+ services: KeepAliveConfig[];
110
+ /** Default options applied to all services unless explicitly overridden */
111
+ defaults?: Partial<KeepAliveConfig>;
112
+ /** Global logger or false */
113
+ logger?: LoggerInterface | false;
114
+ /** Global log level */
115
+ logLevel?: LogLevel;
116
+ }
117
+ /**
118
+ * Resolved internal configuration with guaranteed types and parsed milliseconds.
119
+ */
120
+ interface ResolvedServiceConfig {
121
+ name: string;
122
+ url: string;
123
+ intervalMs: number;
124
+ timeoutMs: number;
125
+ method: HttpMethod;
126
+ headers: Record<string, string>;
127
+ body: string | null;
128
+ retries: number;
129
+ retryDelayMs: number;
130
+ retryStrategy: RetryStrategy;
131
+ retryJitter: boolean;
132
+ maxRetryDelayMs: number;
133
+ expectedStatusCodes: number[] | ((status: number) => boolean);
134
+ logger: LoggerInterface | null;
135
+ logLevel: LogLevel;
136
+ unrefTimer: boolean;
137
+ }
138
+ /**
139
+ * CLI Options parsed from command line flags.
140
+ */
141
+ interface CliOptions {
142
+ url?: string;
143
+ interval?: string;
144
+ timeout?: string;
145
+ method?: string;
146
+ retries?: string | number;
147
+ retryDelay?: string;
148
+ headers?: string[];
149
+ config?: string;
150
+ quiet?: boolean;
151
+ verbose?: boolean;
152
+ once?: boolean;
153
+ help?: boolean;
154
+ version?: boolean;
155
+ }
156
+ /**
157
+ * Events emitted by the KeepAlive instance.
158
+ */
159
+ interface KeepAliveEvents {
160
+ start: (serviceName: string) => void;
161
+ stop: (serviceName: string) => void;
162
+ ping: (info: {
163
+ name: string;
164
+ url: string;
165
+ attempt: number;
166
+ }) => void;
167
+ success: (result: PingResult) => void;
168
+ failure: (result: PingResult) => void;
169
+ retry: (info: RetryInfo) => void;
170
+ error: (error: Error, serviceName: string) => void;
171
+ }
172
+
173
+ declare class KeepAlive extends EventEmitter {
174
+ private config;
175
+ private running;
176
+ private timer;
177
+ private inFlightAbortController;
178
+ private logger;
179
+ constructor(options: KeepAliveConfig);
180
+ private resolveConfig;
181
+ /**
182
+ * Returns whether the keep-alive scheduler is actively running.
183
+ */
184
+ isRunning(): boolean;
185
+ /**
186
+ * Returns the resolved service configuration.
187
+ */
188
+ getConfig(): Readonly<ResolvedServiceConfig>;
189
+ /**
190
+ * Starts the keep-alive scheduler.
191
+ * Performs an immediate ping, then continues at the configured interval.
192
+ */
193
+ start(): this;
194
+ /**
195
+ * Stops the keep-alive scheduler and aborts any active requests or wait timers.
196
+ */
197
+ stop(): Promise<void>;
198
+ /**
199
+ * Executes a single ping cycle with retries without starting the recurring scheduler.
200
+ */
201
+ pingOnce(): Promise<PingResult>;
202
+ /**
203
+ * Core scheduling loop. Executes a ping cycle, then schedules the next interval.
204
+ */
205
+ private runPingLoop;
206
+ /**
207
+ * Executes the ping request, retrying on failure up to configured retry attempts.
208
+ */
209
+ private executePingWithRetries;
210
+ on<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
211
+ once<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
212
+ off<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
213
+ emit<K extends keyof KeepAliveEvents>(event: K, ...args: Parameters<KeepAliveEvents[K]>): boolean;
214
+ }
215
+
216
+ declare class MultiKeepAlive extends EventEmitter {
217
+ private services;
218
+ private serviceMap;
219
+ private logger;
220
+ constructor(options: MultiKeepAliveConfig);
221
+ private forwardEvents;
222
+ /**
223
+ * Starts all configured services.
224
+ */
225
+ start(): this;
226
+ /**
227
+ * Stops all running services.
228
+ */
229
+ stop(): Promise<void>;
230
+ /**
231
+ * Pings all services once in parallel and returns their results.
232
+ */
233
+ pingOnce(): Promise<PingResult[]>;
234
+ /**
235
+ * Returns whether any of the services are currently running.
236
+ */
237
+ isRunning(): boolean;
238
+ /**
239
+ * Returns the list of all registered KeepAlive instances.
240
+ */
241
+ getServices(): KeepAlive[];
242
+ /**
243
+ * Returns a specific KeepAlive instance by service name.
244
+ */
245
+ getService(name: string): KeepAlive | undefined;
246
+ on<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
247
+ once<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
248
+ off<K extends keyof KeepAliveEvents>(event: K, listener: KeepAliveEvents[K]): this;
249
+ emit<K extends keyof KeepAliveEvents>(event: K, ...args: Parameters<KeepAliveEvents[K]>): boolean;
250
+ }
251
+
252
+ interface ExecutePingOptions {
253
+ serviceName: string;
254
+ url: string;
255
+ method: HttpMethod;
256
+ headers?: Record<string, string>;
257
+ body?: string | null;
258
+ timeoutMs: number;
259
+ attempt?: number;
260
+ expectedStatusCodes?: number[] | ((status: number) => boolean);
261
+ externalSignal?: AbortSignal;
262
+ }
263
+ /**
264
+ * Checks whether an HTTP status code is considered successful.
265
+ */
266
+ declare function isStatusSuccessful(status: number, expected?: number[] | ((status: number) => boolean)): boolean;
267
+ /**
268
+ * Executes a single HTTP ping request with timeout and cancellation protection.
269
+ */
270
+ declare function executePing(options: ExecutePingOptions): Promise<PingResult>;
271
+
272
+ /**
273
+ * Parses a human-readable duration string or millisecond number into milliseconds.
274
+ *
275
+ * Supported formats:
276
+ * - Number: `5000` (treated as 5000ms)
277
+ * - String: `'10s'`, `'30s'`, `'1m'`, `'5m'`, `'10m'`, `'1h'`, `'500ms'`, `'1m30s'`
278
+ *
279
+ * @throws {TypeError | RangeError} If the duration is invalid, zero, or negative.
280
+ */
281
+ declare function parseDuration(value: string | number, fieldName?: string): number;
282
+ /**
283
+ * Formats a duration in milliseconds into a concise human-readable string.
284
+ *
285
+ * Examples:
286
+ * - `142` -> `'142ms'`
287
+ * - `5000` -> `'5s'`
288
+ * - `60000` -> `'1m'`
289
+ * - `90000` -> `'1m 30s'`
290
+ * - `3600000` -> `'1h'`
291
+ */
292
+ declare function formatDuration(ms: number): string;
293
+
294
+ interface BackoffOptions {
295
+ attempt: number;
296
+ baseDelayMs: number;
297
+ strategy?: RetryStrategy;
298
+ maxDelayMs?: number;
299
+ jitter?: boolean;
300
+ }
301
+ /**
302
+ * Calculates the backoff delay in milliseconds for a given attempt.
303
+ *
304
+ * @param options Calculation parameters
305
+ * @returns Delay in milliseconds
306
+ */
307
+ declare function calculateBackoff(options: BackoffOptions): number;
308
+ /**
309
+ * Asynchronously sleeps for the given duration, cancellable via AbortSignal.
310
+ *
311
+ * @param ms Duration in milliseconds
312
+ * @param signal Optional AbortSignal to cancel waiting
313
+ */
314
+ declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
315
+
316
+ /**
317
+ * Returns a shallow copy of headers with sensitive values masked.
318
+ */
319
+ declare function redactHeaders(headers?: Record<string, string> | null): Record<string, string>;
320
+ /**
321
+ * Masks sensitive query parameters in a URL string for safe logging.
322
+ */
323
+ declare function redactUrl(urlStr: string): string;
324
+
325
+ declare function formatTimestamp(date?: Date): string;
326
+ declare class Logger implements LoggerInterface {
327
+ private logLevel;
328
+ private prefix?;
329
+ constructor(options?: {
330
+ logLevel?: LogLevel;
331
+ prefix?: string;
332
+ });
333
+ setLevel(level: LogLevel): void;
334
+ setPrefix(prefix: string | undefined): void;
335
+ private shouldLog;
336
+ private formatLine;
337
+ info(message: string, ...args: unknown[]): void;
338
+ success(message: string, ...args: unknown[]): void;
339
+ warn(message: string, ...args: unknown[]): void;
340
+ error(message: string, ...args: unknown[]): void;
341
+ retry(message: string, ...args: unknown[]): void;
342
+ debug(message: string, ...args: unknown[]): void;
343
+ }
344
+
345
+ interface LoadedConfig {
346
+ services: KeepAliveConfig[];
347
+ defaults?: Partial<KeepAliveConfig>;
348
+ logLevel?: LogLevel;
349
+ }
350
+ /**
351
+ * Parses header strings in "Key: Value" or "Key=Value" format.
352
+ */
353
+ declare function parseHeaderPairs(headers: string[] | undefined): Record<string, string>;
354
+ /**
355
+ * Loads configuration from a file path.
356
+ */
357
+ declare function loadConfigFile(customPath?: string): Promise<LoadedConfig | null>;
358
+ /**
359
+ * Normalizes raw config object into LoadedConfig format.
360
+ */
361
+ declare function normalizeRawConfig(raw: unknown): LoadedConfig;
362
+ /**
363
+ * Reads environment variables into a partial KeepAliveConfig.
364
+ */
365
+ declare function loadEnvConfig(): Partial<KeepAliveConfig>;
366
+ /**
367
+ * Resolves complete runtime configuration from CLI flags, config file, and env variables.
368
+ */
369
+ declare function resolveRuntimeConfig(cliOptions: CliOptions): Promise<MultiKeepAliveConfig | KeepAliveConfig>;
370
+
371
+ declare function parseArgs(argv: string[]): CliOptions;
372
+ declare function printHelp(): void;
373
+ declare function runCli(argv?: string[]): Promise<number>;
374
+
375
+ export { type BackoffOptions, type CliOptions, type ExecutePingOptions, type HttpMethod, KeepAlive, type KeepAliveConfig, type KeepAliveEvents, type LoadedConfig, type LogLevel, Logger, type LoggerInterface, MultiKeepAlive, type MultiKeepAliveConfig, type PingResult, type ResolvedServiceConfig, type RetryInfo, type RetryStrategy, calculateBackoff, executePing, formatDuration, formatTimestamp, isStatusSuccessful, loadConfigFile, loadEnvConfig, normalizeRawConfig, parseArgs, parseDuration, parseHeaderPairs, printHelp, redactHeaders, redactUrl, resolveRuntimeConfig, runCli, sleep };