···1414import {
1515 ActionBot,
1616 CronBot,
1717- KexwordBot
1717+ KeywordBot
1818} from "bskybot";
19192020const actionBot: ActionBot = {
···2323 username: "[handle]", // optional for logging needed
2424 service: "https://bsky.social", // or another
2525 action: async (agent: AtpAgent) => {
2626- // implement any logic you want here to be repeated at the scheduledExpression
2626+ // implement any logic you want here
2727 const text = "implement logic to return a string";
2828- console.info(new Date, `Post cronbot ${bot.identifier}: ${text}`)
2828+ console.info(new Date(), `Post actionbot ${actionBot.identifier}: ${text}`);
2929 agent.post({text});
3030 }
3131}
···4949 action: async (agent: AtpAgent) => {
5050 // implement any logic you want here to be executed in your project
5151 const text = "implement logic to return a string";
5252- console.info(new Date, `Post cronbot ${bot.identifier}: ${text}`)
5252+ console.info(new Date(), `Post cronbot ${cronBot.identifier}: ${text}`);
5353 agent.post({text});
5454 }
5555}
···7979}
80808181const keywordBotAgent = useKeywordBotAgent(keywordBot);
8282-```8282+```
8383+8484+## Migration Guide
8585+8686+### Migrating from v1.x to v2.0.0
8787+8888+Version 2.0.0 introduces breaking changes that require code updates when upgrading from v1.x.
8989+9090+#### Breaking Changes
9191+9292+1. **WebSocket Configuration Change**
9393+9494+ The `WebSocketClient` constructor parameter has changed from `url` to `service` and now supports multiple services for failover:
9595+9696+ ```typescript
9797+ // Before (v1.x)
9898+ import { WebSocketClient } from "bskybot";
9999+100100+ const client = new WebSocketClient({
101101+ url: 'wss://example.com'
102102+ });
103103+104104+ // After (v2.0.0)
105105+ const client = new WebSocketClient({
106106+ service: 'wss://example.com' // Single service
107107+ });
108108+109109+ // Or with multiple services for automatic failover:
110110+ const client = new WebSocketClient({
111111+ service: ['wss://primary.com', 'wss://backup.com', 'wss://fallback.com']
112112+ });
113113+ ```
114114+115115+2. **WebSocket Send Method Typing**
116116+117117+ The `send()` method now has stricter typing for better type safety:
118118+119119+ ```typescript
120120+ // Before (v1.x) - accepted any data type
121121+ client.send(anyData);
122122+123123+ // After (v2.0.0) - only accepts specific types
124124+ client.send("string data"); // ✓ Valid
125125+ client.send(buffer); // ✓ Valid (Buffer)
126126+ client.send(arrayBuffer); // ✓ Valid (ArrayBuffer)
127127+ client.send(bufferArray); // ✓ Valid (Buffer[])
128128+ client.send({ custom: "object" }); // ✗ Invalid - will cause TypeScript error
129129+ ```
130130+131131+#### New Features in v2.0.0
132132+133133+- **Multi-Service WebSocket Failover**: Automatically switches between services when connections fail
134134+- **Enhanced Logging**: Structured logging with correlation IDs for better debugging
135135+- **Health Monitoring**: Built-in health checks and metrics collection
136136+- **Performance Optimizations**: Improved error handling and retry strategies
137137+- **Code Quality**: Full ESLint and Prettier integration with pre-commit hooks
138138+139139+#### Configuration Options
140140+141141+The WebSocketClient now supports additional configuration options:
142142+143143+```typescript
144144+const client = new WebSocketClient({
145145+ service: ['wss://primary.com', 'wss://backup.com'],
146146+ maxReconnectAttempts: 3, // Attempts per service (default: 3)
147147+ maxServiceCycles: 2, // Complete cycles through all services (default: 2)
148148+ reconnectInterval: 5000, // Initial delay between attempts (default: 5000ms)
149149+ backoffFactor: 1.5, // Exponential backoff multiplier (default: 1.5)
150150+ maxReconnectDelay: 30000 // Maximum delay between attempts (default: 30000ms)
151151+});
152152+```
153153+154154+All new configuration options are optional and have sensible defaults.
+223-27
dist/index.d.mts
···1919 service: string;
2020};
2121type ActionBot = Bot & {
2222- action: (agent: AtpAgent, params?: any) => Promise<void>;
2222+ action: (agent: AtpAgent, params?: unknown) => Promise<void>;
2323};
2424type CronBot = ActionBot & {
2525 cronJob: Cron;
···7373 collection: string;
7474 rkey: string;
7575 record: {
7676- '$type': string;
7676+ $type: string;
7777 createdAt: string;
7878 subject: string;
7979 reply?: {
···9090 opts: AtpAgentOptions;
9191 actionBot: ActionBot;
9292 constructor(opts: AtpAgentOptions, actionBot: ActionBot);
9393- doAction(params: any): Promise<void>;
9393+ doAction(params?: unknown): Promise<void>;
9494}
9595declare const useActionBotAgent: (actionBot: ActionBot) => Promise<ActionBotAgent | null>;
9696···121121122122interface WebSocketClientOptions {
123123 /** The URL of the WebSocket server to connect to. */
124124- url: string;
124124+ service: string | string[];
125125 /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */
126126 reconnectInterval?: number;
127127 /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */
128128 pingInterval?: number;
129129+ /** Maximum number of consecutive reconnection attempts per service. Default is 3. */
130130+ maxReconnectAttempts?: number;
131131+ /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */
132132+ maxReconnectDelay?: number;
133133+ /** Exponential backoff factor for reconnection delays. Default is 1.5. */
134134+ backoffFactor?: number;
135135+ /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */
136136+ maxServiceCycles?: number;
129137}
130138/**
131139 * A WebSocket client that automatically attempts to reconnect upon disconnection
···135143 * to implement custom handling of WebSocket events.
136144 */
137145declare class WebSocketClient {
138138- private url;
146146+ private service;
139147 private reconnectInterval;
140148 private pingInterval;
141149 private ws;
142150 private pingTimeout;
151151+ private serviceIndex;
152152+ private reconnectAttempts;
153153+ private serviceCycles;
154154+ private maxReconnectAttempts;
155155+ private maxServiceCycles;
156156+ private maxReconnectDelay;
157157+ private backoffFactor;
158158+ private reconnectTimeout;
159159+ private isConnecting;
160160+ private shouldReconnect;
161161+ private messageCount;
162162+ private lastMessageTime;
163163+ private healthCheckName;
143164 /**
144165 * Creates a new instance of `WebSocketClient`.
145166 *
···158179 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
159180 * It clears all event listeners on the old WebSocket and initiates a new connection.
160181 */
161161- private reconnect;
182182+ private scheduleReconnect;
183183+ /**
184184+ * Check if we should try the next service in the array.
185185+ */
186186+ private shouldTryNextService;
187187+ /**
188188+ * Move to the next service in the array and reset reconnection attempts.
189189+ */
190190+ private moveToNextService;
191191+ private cleanup;
162192 /**
163193 * Starts sending periodic ping messages to the server.
164194 *
···183213 *
184214 * Override this method in a subclass to implement custom message handling.
185215 */
186186- protected onMessage(data: WebSocket.Data): void;
216216+ protected onMessage(_data: WebSocket.Data): void;
187217 /**
188218 * Called when a WebSocket error occurs.
189219 *
190220 * @param error - The error that occurred.
191221 *
192222 * Override this method in a subclass to implement custom error handling.
223223+ * Note: Service switching is now handled in the reconnection logic, not here.
193224 */
194194- protected onError(error: Error): void;
225225+ protected onError(_error: Error): void;
195226 /**
196227 * Called when the WebSocket connection is closed.
197228 *
···203234 *
204235 * @param data - The data to send.
205236 */
206206- send(data: any): void;
237237+ send(data: string | Buffer | ArrayBuffer | Buffer[]): void;
207238 /**
208239 * Closes the WebSocket connection gracefully.
209240 */
210241 close(): void;
242242+ getConnectionState(): string;
243243+ getReconnectAttempts(): number;
244244+ getServiceCycles(): number;
245245+ getServiceIndex(): number;
246246+ getAllServices(): string[];
247247+ getCurrentService(): string;
248248+ getMessageCount(): number;
249249+ getLastMessageTime(): number;
250250+ getHealthCheckName(): string;
211251}
212252213253/**
···217257 * It invokes a provided callback function whenever a message is received from the Jetstream server.
218258 */
219259declare class JetstreamSubscription extends WebSocketClient {
220220- service: string;
221260 interval: number;
222261 private onMessageCallback?;
223262 /**
224263 * Creates a new `JetstreamSubscription`.
225264 *
226226- * @param service - The URL of the Jetstream server to connect to.
265265+ * @param service - The URL(-Array) of the Jetstream server(s) to connect to.
227266 * @param interval - The interval (in milliseconds) for reconnect attempts.
228267 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
229268 */
230230- constructor(service: string, interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined);
269269+ constructor(service: string | string[], interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined);
231270 /**
232271 * Called when the WebSocket connection is successfully opened.
233272 * Logs a message indicating that the connection to the Jetstream server has been established.
···255294 protected onClose(): void;
256295}
257296297297+declare enum LogLevel {
298298+ DEBUG = 0,
299299+ INFO = 1,
300300+ WARN = 2,
301301+ ERROR = 3
302302+}
303303+interface LogContext {
304304+ correlationId?: string;
305305+ botId?: string;
306306+ operation?: string;
307307+ duration?: number;
308308+ [key: string]: unknown;
309309+}
258310/**
259259- * A simple logging utility class providing static methods for various log levels.
311311+ * A performance-optimized logging utility class providing static methods for various log levels.
260312 * Each log message is prefixed with a timestamp and log level.
313313+ * Supports conditional logging based on log levels and configurable timezone.
261314 */
262315declare class Logger {
316316+ private static logLevel;
317317+ private static timezone;
318318+ private static correlationId;
319319+ /**
320320+ * Generate a new correlation ID for tracking related operations.
321321+ */
322322+ static generateCorrelationId(): string;
323323+ /**
324324+ * Set the correlation ID for subsequent log entries.
325325+ * @param id - The correlation ID to use, or null to generate a new one
326326+ */
327327+ static setCorrelationId(id?: string | null): void;
328328+ /**
329329+ * Get the current correlation ID.
330330+ */
331331+ static getCorrelationId(): string | null;
332332+ /**
333333+ * Clear the current correlation ID.
334334+ */
335335+ static clearCorrelationId(): void;
336336+ /**
337337+ * Set the minimum log level. Messages below this level will not be logged.
338338+ * @param level - The minimum log level
339339+ */
340340+ static setLogLevel(level: LogLevel): void;
341341+ /**
342342+ * Set the timezone for log timestamps.
343343+ * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC")
344344+ */
345345+ static setTimezone(timezone: string): void;
346346+ /**
347347+ * Get the current log level.
348348+ */
349349+ static getLogLevel(): LogLevel;
350350+ /**
351351+ * Generate a formatted timestamp string.
352352+ * @private
353353+ */
354354+ private static getTimestamp;
355355+ /**
356356+ * Internal logging method that checks log level before processing.
357357+ * @private
358358+ */
359359+ private static log;
263360 /**
264361 * Logs an informational message to the console.
265362 *
266363 * @param message - The message to be logged.
267267- * @param context - Optional additional context (object or string) to log alongside the message.
364364+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
268365 */
269269- static info(message: string, context?: object | string): void;
366366+ static info(message: string, context?: LogContext | object | string): void;
270367 /**
271368 * Logs a warning message to the console.
272369 *
273370 * @param message - The message to be logged.
274274- * @param context - Optional additional context (object or string) to log alongside the message.
371371+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
275372 */
276276- static warn(message: string, context?: object | string): void;
373373+ static warn(message: string, context?: LogContext | object | string): void;
277374 /**
278375 * Logs an error message to the console.
279376 *
280377 * @param message - The message to be logged.
281281- * @param context - Optional additional context (object or string) to log alongside the message.
378378+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
282379 */
283283- static error(message: string, context?: object | string): void;
380380+ static error(message: string, context?: LogContext | object | string): void;
284381 /**
285382 * Logs a debug message to the console.
286383 *
287384 * @param message - The message to be logged.
288288- * @param context - Optional additional context (object or string) to log alongside the message.
385385+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
289386 */
290290- static debug(message: string, context?: object | string): void;
387387+ static debug(message: string, context?: LogContext | object | string): void;
388388+ /**
389389+ * Log operation start with timing.
390390+ * @param operation - The operation name
391391+ * @param context - Additional context
392392+ */
393393+ static startOperation(operation: string, context?: LogContext): string;
394394+ /**
395395+ * Log operation completion with timing.
396396+ * @param operation - The operation name
397397+ * @param startTime - The start time from Date.now()
398398+ * @param context - Additional context
399399+ */
400400+ static endOperation(operation: string, startTime: number, context?: LogContext): void;
291401}
292402293403/**
···298408 */
299409declare const maybeStr: (val?: string) => string | undefined;
300410/**
301301-* Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
302302-*
303303-* @param val - The optional string value to parse.
304304-* @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
305305-*/
411411+ * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
412412+ *
413413+ * @param val - The optional string value to parse.
414414+ * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
415415+ */
306416declare const maybeInt: (val?: string) => number | undefined;
307417308418/**
···317427 */
318428declare function websocketToFeedEntry(data: WebSocket.Data): Post | null;
319429320320-export { type ActionBot, ActionBotAgent, type Bot, type BotReply, type CronBot, CronBotAgent, JetstreamSubscription, type KeywordBot, KeywordBotAgent, Logger, type Post, type UriCid, WebSocketClient, type WebsocketMessage, buildReplyToPost, filterBotReplies, maybeInt, maybeStr, useActionBotAgent, useCronBotAgent, useKeywordBotAgent, websocketToFeedEntry };
430430+interface HealthStatus {
431431+ healthy: boolean;
432432+ timestamp: number;
433433+ checks: Record<string, boolean>;
434434+ metrics: Record<string, number>;
435435+ details?: Record<string, unknown>;
436436+}
437437+interface HealthCheckOptions {
438438+ interval?: number;
439439+ timeout?: number;
440440+ retries?: number;
441441+}
442442+/**
443443+ * Health monitoring system for bot components.
444444+ * Provides health checks and basic metrics collection.
445445+ */
446446+declare class HealthMonitor {
447447+ private checks;
448448+ private metrics;
449449+ private lastCheckResults;
450450+ private checkInterval;
451451+ private options;
452452+ constructor(options?: HealthCheckOptions);
453453+ /**
454454+ * Register a health check function.
455455+ * @param name - Unique name for the health check
456456+ * @param checkFn - Function that returns true if healthy
457457+ */
458458+ registerHealthCheck(name: string, checkFn: () => Promise<boolean>): void;
459459+ /**
460460+ * Remove a health check.
461461+ * @param name - Name of the health check to remove
462462+ */
463463+ unregisterHealthCheck(name: string): void;
464464+ /**
465465+ * Set a metric value.
466466+ * @param name - Metric name
467467+ * @param value - Metric value
468468+ */
469469+ setMetric(name: string, value: number): void;
470470+ /**
471471+ * Increment a counter metric.
472472+ * @param name - Metric name
473473+ * @param increment - Value to add (default: 1)
474474+ */
475475+ incrementMetric(name: string, increment?: number): void;
476476+ /**
477477+ * Get current metric value.
478478+ * @param name - Metric name
479479+ * @returns Current value or 0 if not found
480480+ */
481481+ getMetric(name: string): number;
482482+ /**
483483+ * Get all current metrics.
484484+ * @returns Object with all metrics
485485+ */
486486+ getAllMetrics(): Record<string, number>;
487487+ /**
488488+ * Run a single health check with timeout and retries.
489489+ * @private
490490+ */
491491+ private runHealthCheck;
492492+ /**
493493+ * Wrap a promise with a timeout.
494494+ * @private
495495+ */
496496+ private withTimeout;
497497+ /**
498498+ * Run all health checks and return the current health status.
499499+ */
500500+ getHealthStatus(): Promise<HealthStatus>;
501501+ /**
502502+ * Start periodic health monitoring.
503503+ */
504504+ start(): void;
505505+ /**
506506+ * Stop periodic health monitoring.
507507+ */
508508+ stop(): void;
509509+ /**
510510+ * Get a summary of the last health check results.
511511+ */
512512+ getLastCheckSummary(): Record<string, boolean>;
513513+}
514514+declare const healthMonitor: HealthMonitor;
515515+516516+export { type ActionBot, ActionBotAgent, type Bot, type BotReply, type CronBot, CronBotAgent, type HealthCheckOptions, HealthMonitor, type HealthStatus, JetstreamSubscription, type KeywordBot, KeywordBotAgent, type LogContext, LogLevel, Logger, type Post, type UriCid, WebSocketClient, type WebsocketMessage, buildReplyToPost, filterBotReplies, healthMonitor, maybeInt, maybeStr, useActionBotAgent, useCronBotAgent, useKeywordBotAgent, websocketToFeedEntry };
+223-27
dist/index.d.ts
···1919 service: string;
2020};
2121type ActionBot = Bot & {
2222- action: (agent: AtpAgent, params?: any) => Promise<void>;
2222+ action: (agent: AtpAgent, params?: unknown) => Promise<void>;
2323};
2424type CronBot = ActionBot & {
2525 cronJob: Cron;
···7373 collection: string;
7474 rkey: string;
7575 record: {
7676- '$type': string;
7676+ $type: string;
7777 createdAt: string;
7878 subject: string;
7979 reply?: {
···9090 opts: AtpAgentOptions;
9191 actionBot: ActionBot;
9292 constructor(opts: AtpAgentOptions, actionBot: ActionBot);
9393- doAction(params: any): Promise<void>;
9393+ doAction(params?: unknown): Promise<void>;
9494}
9595declare const useActionBotAgent: (actionBot: ActionBot) => Promise<ActionBotAgent | null>;
9696···121121122122interface WebSocketClientOptions {
123123 /** The URL of the WebSocket server to connect to. */
124124- url: string;
124124+ service: string | string[];
125125 /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */
126126 reconnectInterval?: number;
127127 /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */
128128 pingInterval?: number;
129129+ /** Maximum number of consecutive reconnection attempts per service. Default is 3. */
130130+ maxReconnectAttempts?: number;
131131+ /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */
132132+ maxReconnectDelay?: number;
133133+ /** Exponential backoff factor for reconnection delays. Default is 1.5. */
134134+ backoffFactor?: number;
135135+ /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */
136136+ maxServiceCycles?: number;
129137}
130138/**
131139 * A WebSocket client that automatically attempts to reconnect upon disconnection
···135143 * to implement custom handling of WebSocket events.
136144 */
137145declare class WebSocketClient {
138138- private url;
146146+ private service;
139147 private reconnectInterval;
140148 private pingInterval;
141149 private ws;
142150 private pingTimeout;
151151+ private serviceIndex;
152152+ private reconnectAttempts;
153153+ private serviceCycles;
154154+ private maxReconnectAttempts;
155155+ private maxServiceCycles;
156156+ private maxReconnectDelay;
157157+ private backoffFactor;
158158+ private reconnectTimeout;
159159+ private isConnecting;
160160+ private shouldReconnect;
161161+ private messageCount;
162162+ private lastMessageTime;
163163+ private healthCheckName;
143164 /**
144165 * Creates a new instance of `WebSocketClient`.
145166 *
···158179 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
159180 * It clears all event listeners on the old WebSocket and initiates a new connection.
160181 */
161161- private reconnect;
182182+ private scheduleReconnect;
183183+ /**
184184+ * Check if we should try the next service in the array.
185185+ */
186186+ private shouldTryNextService;
187187+ /**
188188+ * Move to the next service in the array and reset reconnection attempts.
189189+ */
190190+ private moveToNextService;
191191+ private cleanup;
162192 /**
163193 * Starts sending periodic ping messages to the server.
164194 *
···183213 *
184214 * Override this method in a subclass to implement custom message handling.
185215 */
186186- protected onMessage(data: WebSocket.Data): void;
216216+ protected onMessage(_data: WebSocket.Data): void;
187217 /**
188218 * Called when a WebSocket error occurs.
189219 *
190220 * @param error - The error that occurred.
191221 *
192222 * Override this method in a subclass to implement custom error handling.
223223+ * Note: Service switching is now handled in the reconnection logic, not here.
193224 */
194194- protected onError(error: Error): void;
225225+ protected onError(_error: Error): void;
195226 /**
196227 * Called when the WebSocket connection is closed.
197228 *
···203234 *
204235 * @param data - The data to send.
205236 */
206206- send(data: any): void;
237237+ send(data: string | Buffer | ArrayBuffer | Buffer[]): void;
207238 /**
208239 * Closes the WebSocket connection gracefully.
209240 */
210241 close(): void;
242242+ getConnectionState(): string;
243243+ getReconnectAttempts(): number;
244244+ getServiceCycles(): number;
245245+ getServiceIndex(): number;
246246+ getAllServices(): string[];
247247+ getCurrentService(): string;
248248+ getMessageCount(): number;
249249+ getLastMessageTime(): number;
250250+ getHealthCheckName(): string;
211251}
212252213253/**
···217257 * It invokes a provided callback function whenever a message is received from the Jetstream server.
218258 */
219259declare class JetstreamSubscription extends WebSocketClient {
220220- service: string;
221260 interval: number;
222261 private onMessageCallback?;
223262 /**
224263 * Creates a new `JetstreamSubscription`.
225264 *
226226- * @param service - The URL of the Jetstream server to connect to.
265265+ * @param service - The URL(-Array) of the Jetstream server(s) to connect to.
227266 * @param interval - The interval (in milliseconds) for reconnect attempts.
228267 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
229268 */
230230- constructor(service: string, interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined);
269269+ constructor(service: string | string[], interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined);
231270 /**
232271 * Called when the WebSocket connection is successfully opened.
233272 * Logs a message indicating that the connection to the Jetstream server has been established.
···255294 protected onClose(): void;
256295}
257296297297+declare enum LogLevel {
298298+ DEBUG = 0,
299299+ INFO = 1,
300300+ WARN = 2,
301301+ ERROR = 3
302302+}
303303+interface LogContext {
304304+ correlationId?: string;
305305+ botId?: string;
306306+ operation?: string;
307307+ duration?: number;
308308+ [key: string]: unknown;
309309+}
258310/**
259259- * A simple logging utility class providing static methods for various log levels.
311311+ * A performance-optimized logging utility class providing static methods for various log levels.
260312 * Each log message is prefixed with a timestamp and log level.
313313+ * Supports conditional logging based on log levels and configurable timezone.
261314 */
262315declare class Logger {
316316+ private static logLevel;
317317+ private static timezone;
318318+ private static correlationId;
319319+ /**
320320+ * Generate a new correlation ID for tracking related operations.
321321+ */
322322+ static generateCorrelationId(): string;
323323+ /**
324324+ * Set the correlation ID for subsequent log entries.
325325+ * @param id - The correlation ID to use, or null to generate a new one
326326+ */
327327+ static setCorrelationId(id?: string | null): void;
328328+ /**
329329+ * Get the current correlation ID.
330330+ */
331331+ static getCorrelationId(): string | null;
332332+ /**
333333+ * Clear the current correlation ID.
334334+ */
335335+ static clearCorrelationId(): void;
336336+ /**
337337+ * Set the minimum log level. Messages below this level will not be logged.
338338+ * @param level - The minimum log level
339339+ */
340340+ static setLogLevel(level: LogLevel): void;
341341+ /**
342342+ * Set the timezone for log timestamps.
343343+ * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC")
344344+ */
345345+ static setTimezone(timezone: string): void;
346346+ /**
347347+ * Get the current log level.
348348+ */
349349+ static getLogLevel(): LogLevel;
350350+ /**
351351+ * Generate a formatted timestamp string.
352352+ * @private
353353+ */
354354+ private static getTimestamp;
355355+ /**
356356+ * Internal logging method that checks log level before processing.
357357+ * @private
358358+ */
359359+ private static log;
263360 /**
264361 * Logs an informational message to the console.
265362 *
266363 * @param message - The message to be logged.
267267- * @param context - Optional additional context (object or string) to log alongside the message.
364364+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
268365 */
269269- static info(message: string, context?: object | string): void;
366366+ static info(message: string, context?: LogContext | object | string): void;
270367 /**
271368 * Logs a warning message to the console.
272369 *
273370 * @param message - The message to be logged.
274274- * @param context - Optional additional context (object or string) to log alongside the message.
371371+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
275372 */
276276- static warn(message: string, context?: object | string): void;
373373+ static warn(message: string, context?: LogContext | object | string): void;
277374 /**
278375 * Logs an error message to the console.
279376 *
280377 * @param message - The message to be logged.
281281- * @param context - Optional additional context (object or string) to log alongside the message.
378378+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
282379 */
283283- static error(message: string, context?: object | string): void;
380380+ static error(message: string, context?: LogContext | object | string): void;
284381 /**
285382 * Logs a debug message to the console.
286383 *
287384 * @param message - The message to be logged.
288288- * @param context - Optional additional context (object or string) to log alongside the message.
385385+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
289386 */
290290- static debug(message: string, context?: object | string): void;
387387+ static debug(message: string, context?: LogContext | object | string): void;
388388+ /**
389389+ * Log operation start with timing.
390390+ * @param operation - The operation name
391391+ * @param context - Additional context
392392+ */
393393+ static startOperation(operation: string, context?: LogContext): string;
394394+ /**
395395+ * Log operation completion with timing.
396396+ * @param operation - The operation name
397397+ * @param startTime - The start time from Date.now()
398398+ * @param context - Additional context
399399+ */
400400+ static endOperation(operation: string, startTime: number, context?: LogContext): void;
291401}
292402293403/**
···298408 */
299409declare const maybeStr: (val?: string) => string | undefined;
300410/**
301301-* Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
302302-*
303303-* @param val - The optional string value to parse.
304304-* @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
305305-*/
411411+ * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
412412+ *
413413+ * @param val - The optional string value to parse.
414414+ * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
415415+ */
306416declare const maybeInt: (val?: string) => number | undefined;
307417308418/**
···317427 */
318428declare function websocketToFeedEntry(data: WebSocket.Data): Post | null;
319429320320-export { type ActionBot, ActionBotAgent, type Bot, type BotReply, type CronBot, CronBotAgent, JetstreamSubscription, type KeywordBot, KeywordBotAgent, Logger, type Post, type UriCid, WebSocketClient, type WebsocketMessage, buildReplyToPost, filterBotReplies, maybeInt, maybeStr, useActionBotAgent, useCronBotAgent, useKeywordBotAgent, websocketToFeedEntry };
430430+interface HealthStatus {
431431+ healthy: boolean;
432432+ timestamp: number;
433433+ checks: Record<string, boolean>;
434434+ metrics: Record<string, number>;
435435+ details?: Record<string, unknown>;
436436+}
437437+interface HealthCheckOptions {
438438+ interval?: number;
439439+ timeout?: number;
440440+ retries?: number;
441441+}
442442+/**
443443+ * Health monitoring system for bot components.
444444+ * Provides health checks and basic metrics collection.
445445+ */
446446+declare class HealthMonitor {
447447+ private checks;
448448+ private metrics;
449449+ private lastCheckResults;
450450+ private checkInterval;
451451+ private options;
452452+ constructor(options?: HealthCheckOptions);
453453+ /**
454454+ * Register a health check function.
455455+ * @param name - Unique name for the health check
456456+ * @param checkFn - Function that returns true if healthy
457457+ */
458458+ registerHealthCheck(name: string, checkFn: () => Promise<boolean>): void;
459459+ /**
460460+ * Remove a health check.
461461+ * @param name - Name of the health check to remove
462462+ */
463463+ unregisterHealthCheck(name: string): void;
464464+ /**
465465+ * Set a metric value.
466466+ * @param name - Metric name
467467+ * @param value - Metric value
468468+ */
469469+ setMetric(name: string, value: number): void;
470470+ /**
471471+ * Increment a counter metric.
472472+ * @param name - Metric name
473473+ * @param increment - Value to add (default: 1)
474474+ */
475475+ incrementMetric(name: string, increment?: number): void;
476476+ /**
477477+ * Get current metric value.
478478+ * @param name - Metric name
479479+ * @returns Current value or 0 if not found
480480+ */
481481+ getMetric(name: string): number;
482482+ /**
483483+ * Get all current metrics.
484484+ * @returns Object with all metrics
485485+ */
486486+ getAllMetrics(): Record<string, number>;
487487+ /**
488488+ * Run a single health check with timeout and retries.
489489+ * @private
490490+ */
491491+ private runHealthCheck;
492492+ /**
493493+ * Wrap a promise with a timeout.
494494+ * @private
495495+ */
496496+ private withTimeout;
497497+ /**
498498+ * Run all health checks and return the current health status.
499499+ */
500500+ getHealthStatus(): Promise<HealthStatus>;
501501+ /**
502502+ * Start periodic health monitoring.
503503+ */
504504+ start(): void;
505505+ /**
506506+ * Stop periodic health monitoring.
507507+ */
508508+ stop(): void;
509509+ /**
510510+ * Get a summary of the last health check results.
511511+ */
512512+ getLastCheckSummary(): Record<string, boolean>;
513513+}
514514+declare const healthMonitor: HealthMonitor;
515515+516516+export { type ActionBot, ActionBotAgent, type Bot, type BotReply, type CronBot, CronBotAgent, type HealthCheckOptions, HealthMonitor, type HealthStatus, JetstreamSubscription, type KeywordBot, KeywordBotAgent, type LogContext, LogLevel, Logger, type Post, type UriCid, WebSocketClient, type WebsocketMessage, buildReplyToPost, filterBotReplies, healthMonitor, maybeInt, maybeStr, useActionBotAgent, useCronBotAgent, useKeywordBotAgent, websocketToFeedEntry };
+585-58
dist/index.js
···33var __defProp = Object.defineProperty;
44var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
55var __getOwnPropNames = Object.getOwnPropertyNames;
66+var __getOwnPropSymbols = Object.getOwnPropertySymbols;
67var __getProtoOf = Object.getPrototypeOf;
78var __hasOwnProp = Object.prototype.hasOwnProperty;
99+var __propIsEnum = Object.prototype.propertyIsEnumerable;
1010+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1111+var __spreadValues = (a, b) => {
1212+ for (var prop in b || (b = {}))
1313+ if (__hasOwnProp.call(b, prop))
1414+ __defNormalProp(a, prop, b[prop]);
1515+ if (__getOwnPropSymbols)
1616+ for (var prop of __getOwnPropSymbols(b)) {
1717+ if (__propIsEnum.call(b, prop))
1818+ __defNormalProp(a, prop, b[prop]);
1919+ }
2020+ return a;
2121+};
822var __export = (target, all) => {
923 for (var name in all)
1024 __defProp(target, name, { get: all[name], enumerable: true });
···5266__export(index_exports, {
5367 ActionBotAgent: () => ActionBotAgent,
5468 CronBotAgent: () => CronBotAgent,
6969+ HealthMonitor: () => HealthMonitor,
5570 JetstreamSubscription: () => JetstreamSubscription,
5671 KeywordBotAgent: () => KeywordBotAgent,
7272+ LogLevel: () => LogLevel,
5773 Logger: () => Logger,
5874 WebSocketClient: () => WebSocketClient,
5975 buildReplyToPost: () => buildReplyToPost,
6076 filterBotReplies: () => filterBotReplies,
7777+ healthMonitor: () => healthMonitor,
6178 maybeInt: () => maybeInt,
6279 maybeStr: () => maybeStr,
6380 useActionBotAgent: () => useActionBotAgent,
···7188var import_api = require("@atproto/api");
72897390// src/utils/logger.ts
9191+var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
9292+ LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
9393+ LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
9494+ LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
9595+ LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
9696+ return LogLevel2;
9797+})(LogLevel || {});
7498var Logger = class {
7599 /**
100100+ * Generate a new correlation ID for tracking related operations.
101101+ */
102102+ static generateCorrelationId() {
103103+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
104104+ }
105105+ /**
106106+ * Set the correlation ID for subsequent log entries.
107107+ * @param id - The correlation ID to use, or null to generate a new one
108108+ */
109109+ static setCorrelationId(id) {
110110+ this.correlationId = id || this.generateCorrelationId();
111111+ }
112112+ /**
113113+ * Get the current correlation ID.
114114+ */
115115+ static getCorrelationId() {
116116+ return this.correlationId;
117117+ }
118118+ /**
119119+ * Clear the current correlation ID.
120120+ */
121121+ static clearCorrelationId() {
122122+ this.correlationId = null;
123123+ }
124124+ /**
125125+ * Set the minimum log level. Messages below this level will not be logged.
126126+ * @param level - The minimum log level
127127+ */
128128+ static setLogLevel(level) {
129129+ this.logLevel = level;
130130+ }
131131+ /**
132132+ * Set the timezone for log timestamps.
133133+ * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC")
134134+ */
135135+ static setTimezone(timezone) {
136136+ this.timezone = timezone;
137137+ }
138138+ /**
139139+ * Get the current log level.
140140+ */
141141+ static getLogLevel() {
142142+ return this.logLevel;
143143+ }
144144+ /**
145145+ * Generate a formatted timestamp string.
146146+ * @private
147147+ */
148148+ static getTimestamp() {
149149+ return (/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: this.timezone });
150150+ }
151151+ /**
152152+ * Internal logging method that checks log level before processing.
153153+ * @private
154154+ */
155155+ static log(level, levelName, message, context, logFn = console.log) {
156156+ if (level < this.logLevel) {
157157+ return;
158158+ }
159159+ const timestamp = this.getTimestamp();
160160+ let formattedMessage = `${timestamp} [${levelName}]`;
161161+ if (this.correlationId) {
162162+ formattedMessage += ` [${this.correlationId}]`;
163163+ }
164164+ if (context && typeof context === "object" && "correlationId" in context && context.correlationId && context.correlationId !== this.correlationId) {
165165+ formattedMessage += ` [${context.correlationId}]`;
166166+ }
167167+ formattedMessage += `: ${message}`;
168168+ if (context) {
169169+ if (typeof context === "object") {
170170+ const logEntry = __spreadValues({
171171+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
172172+ level: levelName,
173173+ message,
174174+ correlationId: this.correlationId
175175+ }, context);
176176+ logFn(formattedMessage, logEntry);
177177+ } else {
178178+ logFn(formattedMessage, context);
179179+ }
180180+ } else {
181181+ logFn(formattedMessage);
182182+ }
183183+ }
184184+ /**
76185 * Logs an informational message to the console.
77186 *
78187 * @param message - The message to be logged.
7979- * @param context - Optional additional context (object or string) to log alongside the message.
188188+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
80189 */
81190 static info(message, context) {
8282- console.info(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [INFO]: ${message}`, context || "");
191191+ this.log(1 /* INFO */, "INFO", message, context, console.info);
83192 }
84193 /**
85194 * Logs a warning message to the console.
86195 *
87196 * @param message - The message to be logged.
8888- * @param context - Optional additional context (object or string) to log alongside the message.
197197+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
89198 */
90199 static warn(message, context) {
9191- console.warn(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [WARNING]: ${message}`, context || "");
200200+ this.log(2 /* WARN */, "WARNING", message, context, console.warn);
92201 }
93202 /**
94203 * Logs an error message to the console.
95204 *
96205 * @param message - The message to be logged.
9797- * @param context - Optional additional context (object or string) to log alongside the message.
206206+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
98207 */
99208 static error(message, context) {
100100- console.error(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [ERROR]: ${message}`, context || "");
209209+ this.log(3 /* ERROR */, "ERROR", message, context, console.error);
101210 }
102211 /**
103212 * Logs a debug message to the console.
104213 *
105214 * @param message - The message to be logged.
106106- * @param context - Optional additional context (object or string) to log alongside the message.
215215+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
107216 */
108217 static debug(message, context) {
109109- console.debug(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [DEBUG]: ${message}`, context || "");
218218+ this.log(0 /* DEBUG */, "DEBUG", message, context, console.debug);
219219+ }
220220+ /**
221221+ * Log operation start with timing.
222222+ * @param operation - The operation name
223223+ * @param context - Additional context
224224+ */
225225+ static startOperation(operation, context) {
226226+ const correlationId = (context == null ? void 0 : context.correlationId) || this.generateCorrelationId();
227227+ this.setCorrelationId(correlationId);
228228+ this.info(`Starting operation: ${operation}`, __spreadValues({
229229+ operation,
230230+ correlationId
231231+ }, context));
232232+ return correlationId;
233233+ }
234234+ /**
235235+ * Log operation completion with timing.
236236+ * @param operation - The operation name
237237+ * @param startTime - The start time from Date.now()
238238+ * @param context - Additional context
239239+ */
240240+ static endOperation(operation, startTime, context) {
241241+ const duration = Date.now() - startTime;
242242+ this.info(`Completed operation: ${operation}`, __spreadValues({
243243+ operation,
244244+ duration: `${duration}ms`
245245+ }, context));
110246 }
111247};
248248+Logger.logLevel = 1 /* INFO */;
249249+Logger.timezone = "Europe/Vienna";
250250+Logger.correlationId = null;
112251113252// src/bots/actionBot.ts
114253var ActionBotAgent = class extends import_api.AtpAgent {
···119258 }
120259 doAction(params) {
121260 return __async(this, null, function* () {
122122- this.actionBot.action(this, params);
261261+ const correlationId = Logger.startOperation("actionBot.doAction", {
262262+ botId: this.actionBot.username || this.actionBot.identifier
263263+ });
264264+ const startTime = Date.now();
265265+ try {
266266+ yield this.actionBot.action(this, params);
267267+ Logger.endOperation("actionBot.doAction", startTime, {
268268+ correlationId,
269269+ botId: this.actionBot.username || this.actionBot.identifier
270270+ });
271271+ } catch (error) {
272272+ Logger.error("Action bot execution failed", {
273273+ correlationId,
274274+ botId: this.actionBot.username || this.actionBot.identifier,
275275+ error: error instanceof Error ? error.message : String(error)
276276+ });
277277+ throw error;
278278+ }
123279 });
124280 }
125281};
126282var useActionBotAgent = (actionBot) => __async(void 0, null, function* () {
127127- var _a, _b, _c;
283283+ var _a;
284284+ const botId = (_a = actionBot.username) != null ? _a : actionBot.identifier;
285285+ const correlationId = Logger.startOperation("initializeActionBot", { botId });
286286+ const startTime = Date.now();
128287 const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);
129288 try {
130130- Logger.info(`Initialize action bot ${(_a = actionBot.username) != null ? _a : actionBot.identifier}`);
131131- const login = yield agent.login({ identifier: actionBot.identifier, password: actionBot.password });
289289+ Logger.info("Initializing action bot", { correlationId, botId });
290290+ const login = yield agent.login({
291291+ identifier: actionBot.identifier,
292292+ password: actionBot.password
293293+ });
132294 if (!login.success) {
133133- Logger.warn(`Failed to login action bot ${(_b = actionBot.username) != null ? _b : actionBot.identifier}`);
295295+ Logger.warn("Action bot login failed", { correlationId, botId });
134296 return null;
135297 }
298298+ Logger.endOperation("initializeActionBot", startTime, { correlationId, botId });
136299 return agent;
137300 } catch (error) {
138138- Logger.error("Failed to initialize action bot:", `${error}, ${(_c = actionBot.username) != null ? _c : actionBot.identifier}`);
301301+ Logger.error("Failed to initialize action bot", {
302302+ correlationId,
303303+ botId,
304304+ error: error.message,
305305+ duration: Date.now() - startTime
306306+ });
139307 return null;
140308 }
141309});
···164332 const agent = new CronBotAgent({ service: cronBot.service }, cronBot);
165333 try {
166334 Logger.info(`Initialize cron bot ${(_a = cronBot.username) != null ? _a : cronBot.identifier}`);
167167- const login = yield agent.login({ identifier: cronBot.identifier, password: cronBot.password });
335335+ const login = yield agent.login({
336336+ identifier: cronBot.identifier,
337337+ password: cronBot.password
338338+ });
168339 if (!login.success) {
169340 Logger.info(`Failed to login cron bot ${(_b = cronBot.username) != null ? _b : cronBot.identifier}`);
170341 return null;
···172343 agent.job.start();
173344 return agent;
174345 } catch (error) {
175175- Logger.error("Failed to initialize cron bot:", `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`);
346346+ Logger.error(
347347+ "Failed to initialize cron bot:",
348348+ `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`
349349+ );
176350 return null;
177351 }
178352});
···209383 message
210384 );
211385 yield Promise.all([this.like(post.uri, post.cid), this.post(reply)]);
212212- Logger.info(`Replied to post: ${post.uri}`, (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier);
386386+ Logger.info(
387387+ `Replied to post: ${post.uri}`,
388388+ (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier
389389+ );
213390 }
214391 } catch (error) {
215215- Logger.error("Error while replying:", `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`);
392392+ Logger.error(
393393+ "Error while replying:",
394394+ `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`
395395+ );
216396 }
217397 });
218398 }
···222402 $type: "app.bsky.feed.post",
223403 text: message,
224404 reply: {
225225- "root": root,
226226- "parent": parent
405405+ root,
406406+ parent
227407 }
228408 };
229409}
230410function filterBotReplies(text, botReplies) {
411411+ const lowerText = text.toLowerCase();
231412 return botReplies.filter((reply) => {
232413 const keyword = reply.keyword.toLowerCase();
233233- const keywordFound = text.toLowerCase().includes(keyword);
234234- if (!keywordFound) {
414414+ if (!lowerText.includes(keyword)) {
235415 return false;
236416 }
237237- if (Array.isArray(reply.exclude) && reply.exclude.length > 0) {
238238- for (const excludeWord of reply.exclude) {
239239- if (text.toLowerCase().includes(excludeWord.toLowerCase())) {
240240- return false;
241241- }
242242- }
417417+ if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) {
418418+ return true;
243419 }
244244- return true;
420420+ const hasExcludedWord = reply.exclude.some(
421421+ (excludeWord) => lowerText.includes(excludeWord.toLowerCase())
422422+ );
423423+ return !hasExcludedWord;
245424 });
246425}
247426var useKeywordBotAgent = (keywordBot) => __async(void 0, null, function* () {
248427 var _a, _b, _c;
249428 const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);
250429 try {
251251- const login = yield agent.login({ identifier: keywordBot.identifier, password: keywordBot.password });
430430+ const login = yield agent.login({
431431+ identifier: keywordBot.identifier,
432432+ password: keywordBot.password
433433+ });
252434 Logger.info(`Initialize keyword bot ${(_a = keywordBot.username) != null ? _a : keywordBot.identifier}`);
253435 if (!login.success) {
254436 Logger.warn(`Failed to login keyword bot ${(_b = keywordBot.username) != null ? _b : keywordBot.identifier}`);
···256438 }
257439 return agent;
258440 } catch (error) {
259259- Logger.error("Failed to initialize keyword bot:", `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`);
441441+ Logger.error(
442442+ "Failed to initialize keyword bot:",
443443+ `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`
444444+ );
260445 return null;
261446 }
262447});
263448264449// src/utils/websocketClient.ts
265450var import_ws = __toESM(require("ws"));
451451+452452+// src/utils/healthCheck.ts
453453+var HealthMonitor = class {
454454+ constructor(options = {}) {
455455+ this.checks = /* @__PURE__ */ new Map();
456456+ this.metrics = /* @__PURE__ */ new Map();
457457+ this.lastCheckResults = /* @__PURE__ */ new Map();
458458+ this.checkInterval = null;
459459+ this.options = {
460460+ interval: options.interval || 3e4,
461461+ // 30 seconds
462462+ timeout: options.timeout || 5e3,
463463+ // 5 seconds
464464+ retries: options.retries || 2
465465+ };
466466+ }
467467+ /**
468468+ * Register a health check function.
469469+ * @param name - Unique name for the health check
470470+ * @param checkFn - Function that returns true if healthy
471471+ */
472472+ registerHealthCheck(name, checkFn) {
473473+ this.checks.set(name, checkFn);
474474+ Logger.debug(`Registered health check: ${name}`);
475475+ }
476476+ /**
477477+ * Remove a health check.
478478+ * @param name - Name of the health check to remove
479479+ */
480480+ unregisterHealthCheck(name) {
481481+ this.checks.delete(name);
482482+ this.lastCheckResults.delete(name);
483483+ Logger.debug(`Unregistered health check: ${name}`);
484484+ }
485485+ /**
486486+ * Set a metric value.
487487+ * @param name - Metric name
488488+ * @param value - Metric value
489489+ */
490490+ setMetric(name, value) {
491491+ this.metrics.set(name, value);
492492+ }
493493+ /**
494494+ * Increment a counter metric.
495495+ * @param name - Metric name
496496+ * @param increment - Value to add (default: 1)
497497+ */
498498+ incrementMetric(name, increment = 1) {
499499+ const current = this.metrics.get(name) || 0;
500500+ this.metrics.set(name, current + increment);
501501+ }
502502+ /**
503503+ * Get current metric value.
504504+ * @param name - Metric name
505505+ * @returns Current value or 0 if not found
506506+ */
507507+ getMetric(name) {
508508+ return this.metrics.get(name) || 0;
509509+ }
510510+ /**
511511+ * Get all current metrics.
512512+ * @returns Object with all metrics
513513+ */
514514+ getAllMetrics() {
515515+ return Object.fromEntries(this.metrics);
516516+ }
517517+ /**
518518+ * Run a single health check with timeout and retries.
519519+ * @private
520520+ */
521521+ runHealthCheck(name, checkFn) {
522522+ return __async(this, null, function* () {
523523+ for (let attempt = 0; attempt <= this.options.retries; attempt++) {
524524+ try {
525525+ const result = yield this.withTimeout(checkFn(), this.options.timeout);
526526+ if (result) {
527527+ return true;
528528+ }
529529+ } catch (error) {
530530+ Logger.debug(
531531+ `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`,
532532+ { error: error.message }
533533+ );
534534+ }
535535+ }
536536+ return false;
537537+ });
538538+ }
539539+ /**
540540+ * Wrap a promise with a timeout.
541541+ * @private
542542+ */
543543+ withTimeout(promise, timeoutMs) {
544544+ return Promise.race([
545545+ promise,
546546+ new Promise(
547547+ (_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)
548548+ )
549549+ ]);
550550+ }
551551+ /**
552552+ * Run all health checks and return the current health status.
553553+ */
554554+ getHealthStatus() {
555555+ return __async(this, null, function* () {
556556+ const timestamp = Date.now();
557557+ const checkResults = {};
558558+ const details = {};
559559+ const checkPromises = Array.from(this.checks.entries()).map((_0) => __async(this, [_0], function* ([name, checkFn]) {
560560+ const result = yield this.runHealthCheck(name, checkFn);
561561+ checkResults[name] = result;
562562+ this.lastCheckResults.set(name, result);
563563+ if (!result) {
564564+ details[`${name}_last_failure`] = (/* @__PURE__ */ new Date()).toISOString();
565565+ }
566566+ return result;
567567+ }));
568568+ yield Promise.allSettled(checkPromises);
569569+ const healthy = Object.values(checkResults).every((result) => result);
570570+ const metrics = this.getAllMetrics();
571571+ return {
572572+ healthy,
573573+ timestamp,
574574+ checks: checkResults,
575575+ metrics,
576576+ details
577577+ };
578578+ });
579579+ }
580580+ /**
581581+ * Start periodic health monitoring.
582582+ */
583583+ start() {
584584+ if (this.checkInterval) {
585585+ this.stop();
586586+ }
587587+ Logger.info(`Starting health monitor with ${this.options.interval}ms interval`);
588588+ this.checkInterval = setInterval(() => __async(this, null, function* () {
589589+ try {
590590+ const status = yield this.getHealthStatus();
591591+ if (!status.healthy) {
592592+ const failedChecks = Object.entries(status.checks).filter(([, healthy]) => !healthy).map(([name]) => name);
593593+ Logger.warn(`Health check failed`, {
594594+ operation: "health_check",
595595+ failed_checks: failedChecks,
596596+ metrics: status.metrics
597597+ });
598598+ } else {
599599+ Logger.debug("Health check passed", {
600600+ operation: "health_check",
601601+ metrics: status.metrics
602602+ });
603603+ }
604604+ } catch (error) {
605605+ Logger.error("Error during health check:", { error: error.message });
606606+ }
607607+ }), this.options.interval);
608608+ }
609609+ /**
610610+ * Stop periodic health monitoring.
611611+ */
612612+ stop() {
613613+ if (this.checkInterval) {
614614+ clearInterval(this.checkInterval);
615615+ this.checkInterval = null;
616616+ Logger.info("Stopped health monitor");
617617+ }
618618+ }
619619+ /**
620620+ * Get a summary of the last health check results.
621621+ */
622622+ getLastCheckSummary() {
623623+ return Object.fromEntries(this.lastCheckResults);
624624+ }
625625+};
626626+var healthMonitor = new HealthMonitor();
627627+628628+// src/utils/websocketClient.ts
266629var WebSocketClient = class {
267630 /**
268631 * Creates a new instance of `WebSocketClient`.
269269- *
632632+ *
270633 * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.
271634 */
272635 constructor(options) {
273636 this.ws = null;
274637 this.pingTimeout = null;
275275- this.url = options.url;
638638+ this.serviceIndex = 0;
639639+ this.reconnectAttempts = 0;
640640+ this.serviceCycles = 0;
641641+ this.reconnectTimeout = null;
642642+ this.isConnecting = false;
643643+ this.shouldReconnect = true;
644644+ this.messageCount = 0;
645645+ this.lastMessageTime = 0;
646646+ this.service = options.service;
276647 this.reconnectInterval = options.reconnectInterval || 5e3;
277648 this.pingInterval = options.pingInterval || 1e4;
649649+ this.maxReconnectAttempts = options.maxReconnectAttempts || 3;
650650+ this.maxServiceCycles = options.maxServiceCycles || 2;
651651+ this.maxReconnectDelay = options.maxReconnectDelay || 3e4;
652652+ this.backoffFactor = options.backoffFactor || 1.5;
653653+ this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
654654+ healthMonitor.registerHealthCheck(this.healthCheckName, () => __async(this, null, function* () {
655655+ return this.getConnectionState() === "CONNECTED";
656656+ }));
657657+ healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0);
658658+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0);
278659 this.run();
279660 }
280661 /**
281662 * Initiates a WebSocket connection to the specified URL.
282282- *
663663+ *
283664 * This method sets up event listeners for `open`, `message`, `error`, and `close` events.
284665 * When the connection opens, it starts the heartbeat mechanism.
285666 * On close, it attempts to reconnect after a specified interval.
286667 */
287668 run() {
288288- this.ws = new import_ws.default(this.url);
669669+ if (this.isConnecting) {
670670+ return;
671671+ }
672672+ this.isConnecting = true;
673673+ const currentService = Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;
674674+ Logger.info(`Attempting to connect to WebSocket: ${currentService}`);
675675+ this.ws = new import_ws.default(currentService);
289676 this.ws.on("open", () => {
290290- Logger.info("WebSocket connected");
677677+ Logger.info("WebSocket connected successfully", {
678678+ service: this.getCurrentService(),
679679+ serviceIndex: this.serviceIndex
680680+ });
681681+ this.isConnecting = false;
682682+ this.reconnectAttempts = 0;
683683+ this.serviceCycles = 0;
684684+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
291685 this.startHeartbeat();
292686 this.onOpen();
293687 });
294688 this.ws.on("message", (data) => {
689689+ this.messageCount++;
690690+ this.lastMessageTime = Date.now();
691691+ healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`);
295692 this.onMessage(data);
296693 });
297694 this.ws.on("error", (error) => {
298695 Logger.error("WebSocket error:", error);
696696+ this.isConnecting = false;
299697 this.onError(error);
300698 });
301301- this.ws.on("close", () => {
302302- Logger.info("WebSocket disconnected");
699699+ this.ws.on("close", (code, reason) => {
700700+ Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`);
701701+ this.isConnecting = false;
303702 this.stopHeartbeat();
304703 this.onClose();
305305- this.reconnect();
704704+ if (this.shouldReconnect) {
705705+ this.scheduleReconnect();
706706+ }
306707 });
307708 }
308709 /**
309710 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
310711 * It clears all event listeners on the old WebSocket and initiates a new connection.
311712 */
312312- reconnect() {
713713+ scheduleReconnect() {
714714+ this.reconnectAttempts++;
715715+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
716716+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
717717+ if (this.shouldTryNextService()) {
718718+ this.moveToNextService();
719719+ return;
720720+ } else {
721721+ Logger.error("All services exhausted after maximum cycles", {
722722+ totalServices: Array.isArray(this.service) ? this.service.length : 1,
723723+ maxServiceCycles: this.maxServiceCycles,
724724+ serviceCycles: this.serviceCycles
725725+ });
726726+ return;
727727+ }
728728+ }
729729+ const delay = Math.min(
730730+ this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1),
731731+ this.maxReconnectDelay
732732+ );
733733+ Logger.info(
734734+ `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`,
735735+ {
736736+ service: this.getCurrentService(),
737737+ serviceIndex: this.serviceIndex,
738738+ delay: `${delay}ms`
739739+ }
740740+ );
741741+ if (this.reconnectTimeout) {
742742+ clearTimeout(this.reconnectTimeout);
743743+ }
744744+ this.reconnectTimeout = setTimeout(() => {
745745+ this.cleanup();
746746+ this.run();
747747+ }, delay);
748748+ }
749749+ /**
750750+ * Check if we should try the next service in the array.
751751+ */
752752+ shouldTryNextService() {
753753+ if (!Array.isArray(this.service)) {
754754+ return false;
755755+ }
756756+ return this.serviceCycles < this.maxServiceCycles;
757757+ }
758758+ /**
759759+ * Move to the next service in the array and reset reconnection attempts.
760760+ */
761761+ moveToNextService() {
762762+ if (!Array.isArray(this.service)) {
763763+ return;
764764+ }
765765+ const previousIndex = this.serviceIndex;
766766+ this.serviceIndex = (this.serviceIndex + 1) % this.service.length;
767767+ if (this.serviceIndex === 0) {
768768+ this.serviceCycles++;
769769+ }
770770+ this.reconnectAttempts = 0;
771771+ Logger.info("Switching to next service", {
772772+ previousService: this.service[previousIndex],
773773+ previousIndex,
774774+ newService: this.getCurrentService(),
775775+ newIndex: this.serviceIndex,
776776+ serviceCycle: this.serviceCycles
777777+ });
778778+ this.cleanup();
779779+ this.run();
780780+ }
781781+ cleanup() {
313782 if (this.ws) {
314783 this.ws.removeAllListeners();
784784+ if (this.ws.readyState === import_ws.default.OPEN) {
785785+ this.ws.close();
786786+ }
315787 this.ws = null;
316788 }
317317- setTimeout(() => this.run(), this.reconnectInterval);
789789+ if (this.reconnectTimeout) {
790790+ clearTimeout(this.reconnectTimeout);
791791+ this.reconnectTimeout = null;
792792+ }
318793 }
319794 /**
320795 * Starts sending periodic ping messages to the server.
321321- *
796796+ *
322797 * This function uses `setInterval` to send a ping at the configured `pingInterval`.
323798 * If the WebSocket is not open, pings are not sent.
324799 */
···340815 }
341816 /**
342817 * Called when the WebSocket connection is successfully opened.
343343- *
818818+ *
344819 * Override this method in a subclass to implement custom logic on connection.
345820 */
346821 onOpen() {
347822 }
348823 /**
349824 * Called when a WebSocket message is received.
350350- *
825825+ *
351826 * @param data - The data received from the WebSocket server.
352352- *
827827+ *
353828 * Override this method in a subclass to implement custom message handling.
354829 */
355355- onMessage(data) {
830830+ onMessage(_data) {
356831 }
357832 /**
358833 * Called when a WebSocket error occurs.
359359- *
834834+ *
360835 * @param error - The error that occurred.
361361- *
836836+ *
362837 * Override this method in a subclass to implement custom error handling.
838838+ * Note: Service switching is now handled in the reconnection logic, not here.
363839 */
364364- onError(error) {
840840+ onError(_error) {
365841 }
366842 /**
367843 * Called when the WebSocket connection is closed.
368368- *
844844+ *
369845 * Override this method in a subclass to implement custom logic on disconnection.
370846 */
371847 onClose() {
372848 }
373849 /**
374850 * Sends data to the connected WebSocket server, if the connection is open.
375375- *
851851+ *
376852 * @param data - The data to send.
377853 */
378854 send(data) {
···384860 * Closes the WebSocket connection gracefully.
385861 */
386862 close() {
863863+ this.shouldReconnect = false;
864864+ this.stopHeartbeat();
865865+ if (this.reconnectTimeout) {
866866+ clearTimeout(this.reconnectTimeout);
867867+ this.reconnectTimeout = null;
868868+ }
387869 if (this.ws) {
388870 this.ws.close();
389871 }
872872+ healthMonitor.unregisterHealthCheck(this.healthCheckName);
873873+ }
874874+ getConnectionState() {
875875+ if (!this.ws) return "DISCONNECTED";
876876+ switch (this.ws.readyState) {
877877+ case import_ws.default.CONNECTING:
878878+ return "CONNECTING";
879879+ case import_ws.default.OPEN:
880880+ return "CONNECTED";
881881+ case import_ws.default.CLOSING:
882882+ return "CLOSING";
883883+ case import_ws.default.CLOSED:
884884+ return "DISCONNECTED";
885885+ default:
886886+ return "UNKNOWN";
887887+ }
888888+ }
889889+ getReconnectAttempts() {
890890+ return this.reconnectAttempts;
891891+ }
892892+ getServiceCycles() {
893893+ return this.serviceCycles;
894894+ }
895895+ getServiceIndex() {
896896+ return this.serviceIndex;
897897+ }
898898+ getAllServices() {
899899+ return Array.isArray(this.service) ? [...this.service] : [this.service];
900900+ }
901901+ getCurrentService() {
902902+ return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;
903903+ }
904904+ getMessageCount() {
905905+ return this.messageCount;
906906+ }
907907+ getLastMessageTime() {
908908+ return this.lastMessageTime;
909909+ }
910910+ getHealthCheckName() {
911911+ return this.healthCheckName;
390912 }
391913};
392914···394916var JetstreamSubscription = class extends WebSocketClient {
395917 /**
396918 * Creates a new `JetstreamSubscription`.
397397- *
398398- * @param service - The URL of the Jetstream server to connect to.
919919+ *
920920+ * @param service - The URL(-Array) of the Jetstream server(s) to connect to.
399921 * @param interval - The interval (in milliseconds) for reconnect attempts.
400922 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
401923 */
402924 constructor(service, interval, onMessageCallback) {
403403- super({ url: service, reconnectInterval: interval });
404404- this.service = service;
925925+ super({ service, reconnectInterval: interval });
405926 this.interval = interval;
406927 this.onMessageCallback = onMessageCallback;
407928 }
···411932 */
412933 onOpen() {
413934 Logger.info("Connected to Jetstream server.");
935935+ super.onOpen();
414936 }
415937 /**
416938 * Called when a WebSocket message is received.
417417- *
939939+ *
418940 * If an `onMessageCallback` was provided, it is invoked with the received data.
419419- *
941941+ *
420942 * @param data - The data received from the Jetstream server.
421943 */
422944 onMessage(data) {
···427949 /**
428950 * Called when a WebSocket error occurs.
429951 * Logs the error message indicating that Jetstream encountered an error.
430430- *
952952+ *
431953 * @param error - The error that occurred.
432954 */
433955 onError(error) {
434956 Logger.error("Jetstream encountered an error:", error);
957957+ super.onError(error);
435958 }
436959 /**
437960 * Called when the WebSocket connection is closed.
···439962 */
440963 onClose() {
441964 Logger.info("Jetstream connection closed.");
965965+ super.onClose();
442966 }
443967};
444968···4759990 && (module.exports = {
4761000 ActionBotAgent,
4771001 CronBotAgent,
10021002+ HealthMonitor,
4781003 JetstreamSubscription,
4791004 KeywordBotAgent,
10051005+ LogLevel,
4801006 Logger,
4811007 WebSocketClient,
4821008 buildReplyToPost,
4831009 filterBotReplies,
10101010+ healthMonitor,
4841011 maybeInt,
4851012 maybeStr,
4861013 useActionBotAgent,
+1-1
dist/index.js.map
···11-{"version":3,"sources":["../src/index.ts","../src/bots/actionBot.ts","../src/utils/logger.ts","../src/bots/cronBot.ts","../src/bots/keywordBot.ts","../src/utils/websocketClient.ts","../src/utils/jetstreamSubscription.ts","../src/utils/strings.ts","../src/utils/wsToFeed.ts"],"sourcesContent":["export * from \"./types/bot\"\nexport * from \"./types/message\"\nexport * from \"./types/post\"\nexport * from \"./bots/actionBot\"\nexport * from \"./bots/cronBot\"\nexport * from \"./bots/keywordBot\"\nexport * from \"./utils/jetstreamSubscription\"\nexport * from \"./utils/logger\"\nexport * from \"./utils/strings\"\nexport * from \"./utils/websocketClient\"\nexport * from \"./utils/wsToFeed\"\n","import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport { Logger } from '../utils/logger';\nimport type { ActionBot } from '../types/bot';\n\nexport class ActionBotAgent extends AtpAgent {\n constructor(public opts: AtpAgentOptions, public actionBot: ActionBot) {\n super(opts);\n }\n\n async doAction(params:any): Promise<void> {\n this.actionBot.action(this, params);\n }\n}\n\nexport const useActionBotAgent = async (actionBot: ActionBot): Promise<ActionBotAgent | null> => {\n const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);\n \n try {\n Logger.info(`Initialize action bot ${actionBot.username ?? actionBot.identifier}`);\n const login = await agent.login({ identifier: actionBot.identifier, password: actionBot.password! });\n if (!login.success) {\n Logger.warn(`Failed to login action bot ${actionBot.username ?? actionBot.identifier}`);\n return null;\n }\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize action bot:\", `${error}, ${actionBot.username ?? actionBot.identifier}`);\n return null;\n }\n};","/**\n * A simple logging utility class providing static methods for various log levels.\n * Each log message is prefixed with a timestamp and log level.\n */\nexport class Logger {\n /**\n * Logs an informational message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static info(message: string, context?: object | string) {\n console.info(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [INFO]: ${message}`, context || '');\n }\n\n /**\n * Logs a warning message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static warn(message: string, context?: object | string) {\n console.warn(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [WARNING]: ${message}`, context || '');\n }\n\n /**\n * Logs an error message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static error(message: string, context?: object | string) {\n console.error(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [ERROR]: ${message}`, context || '');\n }\n\n /**\n * Logs a debug message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static debug(message: string, context?: object | string) {\n console.debug(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [DEBUG]: ${message}`, context || '');\n }\n}","import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport { CronJob } from 'cron';\nimport { Logger } from '../utils/logger';\nimport type { CronBot } from '../types/bot';\n\nexport class CronBotAgent extends AtpAgent {\n public job: CronJob;\n\n constructor(public opts: AtpAgentOptions, public cronBot: CronBot) {\n super(opts);\n\n this.job = new CronJob(\n cronBot.cronJob.scheduleExpression,\n async () => cronBot.action(this),\n cronBot.cronJob.callback,\n false,\n cronBot.cronJob.timeZone,\n );\n }\n}\n\nexport const useCronBotAgent = async (cronBot: CronBot): Promise<CronBotAgent | null> => {\n const agent = new CronBotAgent({ service: cronBot.service }, cronBot);\n \n try {\n Logger.info(`Initialize cron bot ${cronBot.username ?? cronBot.identifier}`);\n const login = await agent.login({ identifier: cronBot.identifier, password: cronBot.password! });\n if (!login.success) {\n Logger.info(`Failed to login cron bot ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n agent.job.start();\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize cron bot:\", `${error}, ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n};","import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport type { BotReply, KeywordBot } from '../types/bot';\nimport type { Post, UriCid } from \"../types/post\";\nimport { Logger } from '../utils/logger';\n\n\nexport class KeywordBotAgent extends AtpAgent {\n constructor(public opts: AtpAgentOptions, public keywordBot: KeywordBot) {\n super(opts);\n }\n \n async likeAndReplyIfFollower(post: Post): Promise<void> {\n if (post.authorDid === this.assertDid) {\n return;\n }\n\n const replies = filterBotReplies(post.text, this.keywordBot.replies);\n if (replies.length < 1) {\n return;\n }\n\n try {\n const actorProfile = await this.getProfile({actor: post.authorDid});\n\n if(actorProfile.success) {\n \n if (!actorProfile.data.viewer?.followedBy) {\n return;\n }\n\n const replyCfg = replies[Math.floor(Math.random() * replies.length)];\n const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)];\n const reply = buildReplyToPost(\n { uri: post.rootUri, cid: post.rootCid },\n { uri: post.uri, cid: post.cid },\n message\n );\n\n await Promise.all([this.like(post.uri, post.cid), this.post(reply)]);\n Logger.info(`Replied to post: ${post.uri}`, this.keywordBot.username ?? this.keywordBot.identifier);\n }\n } catch (error) {\n Logger.error(\"Error while replying:\", `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}`);\n }\n }\n}\n\nexport function buildReplyToPost (root: UriCid, parent: UriCid, message: string) { \n return {\n $type: \"app.bsky.feed.post\" as \"app.bsky.feed.post\",\n text: message,\n reply: {\n \"root\": root,\n \"parent\": parent\n }\n };\n}\n\nexport function filterBotReplies(text: string, botReplies: BotReply[]) {\n return botReplies.filter(reply => {\n const keyword = reply.keyword.toLowerCase();\n const keywordFound = text.toLowerCase().includes(keyword);\n if (!keywordFound) {\n return false;\n }\n\n if (Array.isArray(reply.exclude) && reply.exclude.length > 0) {\n for (const excludeWord of reply.exclude) {\n if (text.toLowerCase().includes(excludeWord.toLowerCase())) {\n return false;\n }\n }\n }\n\n return true;\n });\n}\n\nexport const useKeywordBotAgent = async (keywordBot: KeywordBot): Promise<KeywordBotAgent | null> => {\n const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);\n\n try {\n const login = await agent.login({ identifier: keywordBot.identifier, password: keywordBot.password! });\n\n Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n\n if (!login.success) { \n Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize keyword bot:\", `${error}, ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n};","import WebSocket from 'ws';\nimport { Logger } from './logger';\n\ninterface WebSocketClientOptions {\n /** The URL of the WebSocket server to connect to. */\n url: string;\n /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */\n reconnectInterval?: number;\n /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */\n pingInterval?: number;\n}\n\n/**\n * A WebSocket client that automatically attempts to reconnect upon disconnection\n * and periodically sends ping messages (heartbeats) to ensure the connection remains alive.\n * \n * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods\n * to implement custom handling of WebSocket events.\n */\nexport class WebSocketClient {\n private url: string;\n private reconnectInterval: number;\n private pingInterval: number;\n private ws: WebSocket | null = null;\n private pingTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Creates a new instance of `WebSocketClient`.\n * \n * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.\n */\n constructor(options: WebSocketClientOptions) {\n this.url = options.url;\n this.reconnectInterval = options.reconnectInterval || 5000;\n this.pingInterval = options.pingInterval || 10000; \n this.run();\n }\n\n /**\n * Initiates a WebSocket connection to the specified URL.\n * \n * This method sets up event listeners for `open`, `message`, `error`, and `close` events.\n * When the connection opens, it starts the heartbeat mechanism.\n * On close, it attempts to reconnect after a specified interval.\n */\n private run() {\n this.ws = new WebSocket(this.url);\n\n this.ws.on('open', () => {\n Logger.info('WebSocket connected');\n this.startHeartbeat();\n this.onOpen();\n });\n\n this.ws.on('message', (data: WebSocket.Data) => {\n this.onMessage(data);\n });\n\n this.ws.on('error', (error) => {\n Logger.error('WebSocket error:', error);\n this.onError(error);\n });\n\n this.ws.on('close', () => {\n Logger.info('WebSocket disconnected');\n this.stopHeartbeat();\n this.onClose();\n this.reconnect();\n });\n }\n\n /**\n * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.\n * It clears all event listeners on the old WebSocket and initiates a new connection.\n */\n private reconnect() {\n if (this.ws) {\n this.ws.removeAllListeners();\n this.ws = null;\n }\n\n setTimeout(() => this.run(), this.reconnectInterval);\n }\n\n /**\n * Starts sending periodic ping messages to the server.\n * \n * This function uses `setInterval` to send a ping at the configured `pingInterval`.\n * If the WebSocket is not open, pings are not sent.\n */\n private startHeartbeat() {\n this.pingTimeout = setInterval(() => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.ping(); \n }\n }, this.pingInterval);\n }\n\n /**\n * Stops sending heartbeat pings by clearing the ping interval.\n */\n private stopHeartbeat() {\n if (this.pingTimeout) {\n clearInterval(this.pingTimeout);\n this.pingTimeout = null;\n }\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * \n * Override this method in a subclass to implement custom logic on connection.\n */\n protected onOpen() {\n // Custom logic for connection open\n }\n\n /**\n * Called when a WebSocket message is received.\n * \n * @param data - The data received from the WebSocket server.\n * \n * Override this method in a subclass to implement custom message handling.\n */\n protected onMessage(data: WebSocket.Data) {\n // Custom logic for handling received messages\n }\n\n /**\n * Called when a WebSocket error occurs.\n * \n * @param error - The error that occurred.\n * \n * Override this method in a subclass to implement custom error handling.\n */\n protected onError(error: Error) {\n // Custom logic for handling errors\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * \n * Override this method in a subclass to implement custom logic on disconnection.\n */\n protected onClose() {\n // Custom logic for handling connection close\n }\n\n /**\n * Sends data to the connected WebSocket server, if the connection is open.\n * \n * @param data - The data to send.\n */\n public send(data: any) {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data);\n }\n }\n\n /**\n * Closes the WebSocket connection gracefully.\n */\n public close() {\n if (this.ws) {\n this.ws.close();\n }\n }\n}","import WebSocket from 'ws';\nimport { WebSocketClient } from './websocketClient';\nimport { Logger } from './logger';\n\n/**\n * Represents a subscription to a Jetstream feed over WebSocket.\n * \n * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats.\n * It invokes a provided callback function whenever a message is received from the Jetstream server.\n */\nexport class JetstreamSubscription extends WebSocketClient {\n /**\n * Creates a new `JetstreamSubscription`.\n * \n * @param service - The URL of the Jetstream server to connect to.\n * @param interval - The interval (in milliseconds) for reconnect attempts.\n * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.\n */\n constructor(\n public service: string,\n public interval: number,\n private onMessageCallback?: (data: WebSocket.Data) => void\n ) {\n super({url: service, reconnectInterval: interval});\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * Logs a message indicating that the connection to the Jetstream server has been established.\n */\n protected onOpen() {\n Logger.info('Connected to Jetstream server.');\n }\n\n /**\n * Called when a WebSocket message is received.\n * \n * If an `onMessageCallback` was provided, it is invoked with the received data.\n * \n * @param data - The data received from the Jetstream server.\n */\n protected onMessage(data: WebSocket.Data) {\n if (this.onMessageCallback) {\n this.onMessageCallback(data);\n }\n }\n\n /**\n * Called when a WebSocket error occurs.\n * Logs the error message indicating that Jetstream encountered an error.\n * \n * @param error - The error that occurred.\n */\n protected onError(error: Error) {\n Logger.error('Jetstream encountered an error:', error);\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * Logs a message indicating that the Jetstream connection has closed.\n */\n protected onClose() {\n Logger.info('Jetstream connection closed.');\n }\n}\n","/**\n * Returns the given string if it is defined; otherwise returns `undefined`.\n * \n * @param val - The optional string value to check.\n * @returns The given string if defined, or `undefined` if `val` is falsy.\n */\nexport const maybeStr = (val?: string): string | undefined => {\n if (!val) return undefined;\n return val;\n}\n\n/**\n* Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.\n* \n* @param val - The optional string value to parse.\n* @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.\n*/\nexport const maybeInt = (val?: string): number | undefined => {\n if (!val) return undefined;\n const int = parseInt(val, 10);\n if (isNaN(int)) return undefined;\n return int;\n}","import WebSocket from 'ws';\nimport { Post } from \"../types/post\";\nimport { WebsocketMessage } from '../types/message';\n;\n\n/**\n * Converts a raw WebSocket message into a `FeedEntry` object, if possible.\n * \n * This function checks if the incoming WebSocket data is structured like a feed commit message\n * with the required properties for a created post. If the data matches the expected shape,\n * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`.\n * \n * @param data - The raw WebSocket data.\n * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`.\n */\nexport function websocketToFeedEntry(data: WebSocket.Data): Post | null {\n const message = data as WebsocketMessage;\n if(!message.commit || !message.commit.record || !message.commit.record['$type'] || !message.did || !message.commit.cid || !message.commit.rkey || message.commit.operation !== \"create\") {\n return null;\n }\n const messageUri = `at://${message.did}/${message.commit.record['$type']}/${message.commit.rkey}`;\n return {\n cid: message.commit.cid,\n uri: messageUri,\n authorDid: message.did,\n text: message.commit.record.text,\n rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,\n rootUri: message.commit.record.reply?.root.uri ?? messageUri,\n };\n}"],"mappings":"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","names":["import_api","import_api","WebSocket"]}11+{"version":3,"sources":["../src/index.ts","../src/bots/actionBot.ts","../src/utils/logger.ts","../src/bots/cronBot.ts","../src/bots/keywordBot.ts","../src/utils/websocketClient.ts","../src/utils/healthCheck.ts","../src/utils/jetstreamSubscription.ts","../src/utils/strings.ts","../src/utils/wsToFeed.ts"],"sourcesContent":["export * from \"./types/bot\";\nexport * from \"./types/message\";\nexport * from \"./types/post\";\nexport * from \"./bots/actionBot\";\nexport * from \"./bots/cronBot\";\nexport * from \"./bots/keywordBot\";\nexport * from \"./utils/jetstreamSubscription\";\nexport * from \"./utils/logger\";\nexport * from \"./utils/strings\";\nexport * from \"./utils/websocketClient\";\nexport * from \"./utils/wsToFeed\";\nexport * from \"./utils/healthCheck\";\n","import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport { Logger } from \"../utils/logger\";\nimport type { ActionBot } from \"../types/bot\";\n\nexport class ActionBotAgent extends AtpAgent {\n constructor(\n public opts: AtpAgentOptions,\n public actionBot: ActionBot\n ) {\n super(opts);\n }\n\n async doAction(params?: unknown): Promise<void> {\n const correlationId = Logger.startOperation(\"actionBot.doAction\", {\n botId: this.actionBot.username || this.actionBot.identifier,\n });\n\n const startTime = Date.now();\n\n try {\n await this.actionBot.action(this, params);\n Logger.endOperation(\"actionBot.doAction\", startTime, {\n correlationId,\n botId: this.actionBot.username || this.actionBot.identifier,\n });\n } catch (error) {\n Logger.error(\"Action bot execution failed\", {\n correlationId,\n botId: this.actionBot.username || this.actionBot.identifier,\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n\nexport const useActionBotAgent = async (actionBot: ActionBot): Promise<ActionBotAgent | null> => {\n const botId = actionBot.username ?? actionBot.identifier;\n const correlationId = Logger.startOperation(\"initializeActionBot\", { botId });\n const startTime = Date.now();\n\n const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);\n\n try {\n Logger.info(\"Initializing action bot\", { correlationId, botId });\n\n const login = await agent.login({\n identifier: actionBot.identifier,\n password: actionBot.password!,\n });\n\n if (!login.success) {\n Logger.warn(\"Action bot login failed\", { correlationId, botId });\n return null;\n }\n\n Logger.endOperation(\"initializeActionBot\", startTime, { correlationId, botId });\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize action bot\", {\n correlationId,\n botId,\n error: error.message,\n duration: Date.now() - startTime,\n });\n return null;\n }\n};\n","export enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n}\n\nexport interface LogContext {\n correlationId?: string;\n botId?: string;\n operation?: string;\n duration?: number;\n [key: string]: unknown;\n}\n\n/**\n * A performance-optimized logging utility class providing static methods for various log levels.\n * Each log message is prefixed with a timestamp and log level.\n * Supports conditional logging based on log levels and configurable timezone.\n */\nexport class Logger {\n private static logLevel: LogLevel = LogLevel.INFO;\n private static timezone: string = \"Europe/Vienna\";\n private static correlationId: string | null = null;\n\n /**\n * Generate a new correlation ID for tracking related operations.\n */\n static generateCorrelationId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Set the correlation ID for subsequent log entries.\n * @param id - The correlation ID to use, or null to generate a new one\n */\n static setCorrelationId(id?: string | null) {\n this.correlationId = id || this.generateCorrelationId();\n }\n\n /**\n * Get the current correlation ID.\n */\n static getCorrelationId(): string | null {\n return this.correlationId;\n }\n\n /**\n * Clear the current correlation ID.\n */\n static clearCorrelationId() {\n this.correlationId = null;\n }\n\n /**\n * Set the minimum log level. Messages below this level will not be logged.\n * @param level - The minimum log level\n */\n static setLogLevel(level: LogLevel) {\n this.logLevel = level;\n }\n\n /**\n * Set the timezone for log timestamps.\n * @param timezone - The timezone string (e.g., \"Europe/Vienna\", \"UTC\")\n */\n static setTimezone(timezone: string) {\n this.timezone = timezone;\n }\n\n /**\n * Get the current log level.\n */\n static getLogLevel(): LogLevel {\n return this.logLevel;\n }\n\n /**\n * Generate a formatted timestamp string.\n * @private\n */\n private static getTimestamp(): string {\n return new Date().toLocaleString(\"de-DE\", { timeZone: this.timezone });\n }\n\n /**\n * Internal logging method that checks log level before processing.\n * @private\n */\n private static log(\n level: LogLevel,\n levelName: string,\n message: string,\n context?: LogContext | object | string,\n logFn = console.log\n ) {\n if (level < this.logLevel) {\n return; // Skip logging if below threshold\n }\n\n const timestamp = this.getTimestamp();\n let formattedMessage = `${timestamp} [${levelName}]`;\n\n // Add correlation ID if available\n if (this.correlationId) {\n formattedMessage += ` [${this.correlationId}]`;\n }\n\n // Add context correlation ID if provided and different from global one\n if (\n context &&\n typeof context === \"object\" &&\n \"correlationId\" in context &&\n context.correlationId &&\n context.correlationId !== this.correlationId\n ) {\n formattedMessage += ` [${context.correlationId}]`;\n }\n\n formattedMessage += `: ${message}`;\n\n if (context) {\n // Create structured log entry for objects\n if (typeof context === \"object\") {\n const logEntry = {\n timestamp: new Date().toISOString(),\n level: levelName,\n message,\n correlationId: this.correlationId,\n ...context,\n };\n logFn(formattedMessage, logEntry);\n } else {\n logFn(formattedMessage, context);\n }\n } else {\n logFn(formattedMessage);\n }\n }\n /**\n * Logs an informational message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static info(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.INFO, \"INFO\", message, context, console.info);\n }\n\n /**\n * Logs a warning message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static warn(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.WARN, \"WARNING\", message, context, console.warn);\n }\n\n /**\n * Logs an error message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static error(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.ERROR, \"ERROR\", message, context, console.error);\n }\n\n /**\n * Logs a debug message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static debug(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.DEBUG, \"DEBUG\", message, context, console.debug);\n }\n\n /**\n * Log operation start with timing.\n * @param operation - The operation name\n * @param context - Additional context\n */\n static startOperation(operation: string, context?: LogContext): string {\n const correlationId = context?.correlationId || this.generateCorrelationId();\n this.setCorrelationId(correlationId);\n\n this.info(`Starting operation: ${operation}`, {\n operation,\n correlationId,\n ...context,\n });\n\n return correlationId;\n }\n\n /**\n * Log operation completion with timing.\n * @param operation - The operation name\n * @param startTime - The start time from Date.now()\n * @param context - Additional context\n */\n static endOperation(operation: string, startTime: number, context?: LogContext) {\n const duration = Date.now() - startTime;\n\n this.info(`Completed operation: ${operation}`, {\n operation,\n duration: `${duration}ms`,\n ...context,\n });\n }\n}\n","import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport { CronJob } from \"cron\";\nimport { Logger } from \"../utils/logger\";\nimport type { CronBot } from \"../types/bot\";\n\nexport class CronBotAgent extends AtpAgent {\n public job: CronJob;\n\n constructor(\n public opts: AtpAgentOptions,\n public cronBot: CronBot\n ) {\n super(opts);\n\n this.job = new CronJob(\n cronBot.cronJob.scheduleExpression,\n async () => cronBot.action(this),\n cronBot.cronJob.callback,\n false,\n cronBot.cronJob.timeZone\n );\n }\n}\n\nexport const useCronBotAgent = async (cronBot: CronBot): Promise<CronBotAgent | null> => {\n const agent = new CronBotAgent({ service: cronBot.service }, cronBot);\n\n try {\n Logger.info(`Initialize cron bot ${cronBot.username ?? cronBot.identifier}`);\n const login = await agent.login({\n identifier: cronBot.identifier,\n password: cronBot.password!,\n });\n if (!login.success) {\n Logger.info(`Failed to login cron bot ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n agent.job.start();\n return agent;\n } catch (error) {\n Logger.error(\n \"Failed to initialize cron bot:\",\n `${error}, ${cronBot.username ?? cronBot.identifier}`\n );\n return null;\n }\n};\n","import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport type { BotReply, KeywordBot } from \"../types/bot\";\nimport type { Post, UriCid } from \"../types/post\";\nimport { Logger } from \"../utils/logger\";\n\nexport class KeywordBotAgent extends AtpAgent {\n constructor(\n public opts: AtpAgentOptions,\n public keywordBot: KeywordBot\n ) {\n super(opts);\n }\n\n async likeAndReplyIfFollower(post: Post): Promise<void> {\n if (post.authorDid === this.assertDid) {\n return;\n }\n\n const replies = filterBotReplies(post.text, this.keywordBot.replies);\n if (replies.length < 1) {\n return;\n }\n\n try {\n const actorProfile = await this.getProfile({ actor: post.authorDid });\n\n if (actorProfile.success) {\n if (!actorProfile.data.viewer?.followedBy) {\n return;\n }\n\n const replyCfg = replies[Math.floor(Math.random() * replies.length)];\n const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)];\n const reply = buildReplyToPost(\n { uri: post.rootUri, cid: post.rootCid },\n { uri: post.uri, cid: post.cid },\n message\n );\n\n await Promise.all([this.like(post.uri, post.cid), this.post(reply)]);\n Logger.info(\n `Replied to post: ${post.uri}`,\n this.keywordBot.username ?? this.keywordBot.identifier\n );\n }\n } catch (error) {\n Logger.error(\n \"Error while replying:\",\n `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}`\n );\n }\n }\n}\n\nexport function buildReplyToPost(root: UriCid, parent: UriCid, message: string) {\n return {\n $type: \"app.bsky.feed.post\" as const,\n text: message,\n reply: {\n root: root,\n parent: parent,\n },\n };\n}\n\nexport function filterBotReplies(text: string, botReplies: BotReply[]) {\n // Cache the lowercased text to avoid multiple toLowerCase() calls\n const lowerText = text.toLowerCase();\n\n return botReplies.filter(reply => {\n // Use cached lowercase comparison\n const keyword = reply.keyword.toLowerCase();\n if (!lowerText.includes(keyword)) {\n return false;\n }\n\n // Early return if no exclusions\n if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) {\n return true;\n }\n\n // Use some() for early exit on first match\n const hasExcludedWord = reply.exclude.some(excludeWord =>\n lowerText.includes(excludeWord.toLowerCase())\n );\n\n return !hasExcludedWord;\n });\n}\n\nexport const useKeywordBotAgent = async (\n keywordBot: KeywordBot\n): Promise<KeywordBotAgent | null> => {\n const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);\n\n try {\n const login = await agent.login({\n identifier: keywordBot.identifier,\n password: keywordBot.password!,\n });\n\n Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n\n if (!login.success) {\n Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n\n return agent;\n } catch (error) {\n Logger.error(\n \"Failed to initialize keyword bot:\",\n `${error}, ${keywordBot.username ?? keywordBot.identifier}`\n );\n return null;\n }\n};\n","import WebSocket from \"ws\";\nimport { Logger } from \"./logger\";\nimport { healthMonitor } from \"./healthCheck\";\n\ninterface WebSocketClientOptions {\n /** The URL of the WebSocket server to connect to. */\n service: string | string[];\n /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */\n reconnectInterval?: number;\n /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */\n pingInterval?: number;\n /** Maximum number of consecutive reconnection attempts per service. Default is 3. */\n maxReconnectAttempts?: number;\n /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */\n maxReconnectDelay?: number;\n /** Exponential backoff factor for reconnection delays. Default is 1.5. */\n backoffFactor?: number;\n /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */\n maxServiceCycles?: number;\n}\n\n/**\n * A WebSocket client that automatically attempts to reconnect upon disconnection\n * and periodically sends ping messages (heartbeats) to ensure the connection remains alive.\n *\n * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods\n * to implement custom handling of WebSocket events.\n */\nexport class WebSocketClient {\n private service: string | string[];\n private reconnectInterval: number;\n private pingInterval: number;\n private ws: WebSocket | null = null;\n private pingTimeout: NodeJS.Timeout | null = null;\n private serviceIndex = 0;\n private reconnectAttempts = 0;\n private serviceCycles = 0;\n private maxReconnectAttempts: number;\n private maxServiceCycles: number;\n private maxReconnectDelay: number;\n private backoffFactor: number;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n private isConnecting = false;\n private shouldReconnect = true;\n private messageCount = 0;\n private lastMessageTime = 0;\n private healthCheckName: string;\n\n /**\n * Creates a new instance of `WebSocketClient`.\n *\n * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.\n */\n constructor(options: WebSocketClientOptions) {\n this.service = options.service;\n this.reconnectInterval = options.reconnectInterval || 5000;\n this.pingInterval = options.pingInterval || 10000;\n this.maxReconnectAttempts = options.maxReconnectAttempts || 3;\n this.maxServiceCycles = options.maxServiceCycles || 2;\n this.maxReconnectDelay = options.maxReconnectDelay || 30000;\n this.backoffFactor = options.backoffFactor || 1.5;\n\n // Generate unique health check name\n this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;\n\n // Register health check\n healthMonitor.registerHealthCheck(this.healthCheckName, async () => {\n return this.getConnectionState() === \"CONNECTED\";\n });\n\n // Initialize metrics\n healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0);\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0);\n\n this.run();\n }\n\n /**\n * Initiates a WebSocket connection to the specified URL.\n *\n * This method sets up event listeners for `open`, `message`, `error`, and `close` events.\n * When the connection opens, it starts the heartbeat mechanism.\n * On close, it attempts to reconnect after a specified interval.\n */\n private run() {\n if (this.isConnecting) {\n return;\n }\n\n this.isConnecting = true;\n const currentService = Array.isArray(this.service)\n ? this.service[this.serviceIndex]\n : this.service;\n\n Logger.info(`Attempting to connect to WebSocket: ${currentService}`);\n this.ws = new WebSocket(currentService);\n\n this.ws.on(\"open\", () => {\n Logger.info(\"WebSocket connected successfully\", {\n service: this.getCurrentService(),\n serviceIndex: this.serviceIndex,\n });\n this.isConnecting = false;\n this.reconnectAttempts = 0; // Reset on successful connection\n this.serviceCycles = 0; // Reset cycles on successful connection\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);\n this.startHeartbeat();\n this.onOpen();\n });\n\n this.ws.on(\"message\", (data: WebSocket.Data) => {\n this.messageCount++;\n this.lastMessageTime = Date.now();\n healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`);\n this.onMessage(data);\n });\n\n this.ws.on(\"error\", error => {\n Logger.error(\"WebSocket error:\", error);\n this.isConnecting = false;\n this.onError(error);\n });\n\n this.ws.on(\"close\", (code, reason) => {\n Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`);\n this.isConnecting = false;\n this.stopHeartbeat();\n this.onClose();\n\n if (this.shouldReconnect) {\n this.scheduleReconnect();\n }\n });\n }\n\n /**\n * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.\n * It clears all event listeners on the old WebSocket and initiates a new connection.\n */\n private scheduleReconnect() {\n this.reconnectAttempts++;\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);\n\n // Check if we should try the next service\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n if (this.shouldTryNextService()) {\n this.moveToNextService();\n return; // Try next service immediately\n } else {\n Logger.error(\"All services exhausted after maximum cycles\", {\n totalServices: Array.isArray(this.service) ? this.service.length : 1,\n maxServiceCycles: this.maxServiceCycles,\n serviceCycles: this.serviceCycles,\n });\n return; // Give up entirely\n }\n }\n\n const delay = Math.min(\n this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1),\n this.maxReconnectDelay\n );\n\n Logger.info(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`,\n {\n service: this.getCurrentService(),\n serviceIndex: this.serviceIndex,\n delay: `${delay}ms`,\n }\n );\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n }\n\n this.reconnectTimeout = setTimeout(() => {\n this.cleanup();\n this.run();\n }, delay);\n }\n\n /**\n * Check if we should try the next service in the array.\n */\n private shouldTryNextService(): boolean {\n if (!Array.isArray(this.service)) {\n return false; // Single service, can't switch\n }\n\n return this.serviceCycles < this.maxServiceCycles;\n }\n\n /**\n * Move to the next service in the array and reset reconnection attempts.\n */\n private moveToNextService() {\n if (!Array.isArray(this.service)) {\n return;\n }\n\n const previousIndex = this.serviceIndex;\n this.serviceIndex = (this.serviceIndex + 1) % this.service.length;\n\n // If we've gone through all services once, increment the cycle counter\n if (this.serviceIndex === 0) {\n this.serviceCycles++;\n }\n\n this.reconnectAttempts = 0; // Reset attempts for the new service\n\n Logger.info(\"Switching to next service\", {\n previousService: this.service[previousIndex],\n previousIndex,\n newService: this.getCurrentService(),\n newIndex: this.serviceIndex,\n serviceCycle: this.serviceCycles,\n });\n\n // Try the new service immediately\n this.cleanup();\n this.run();\n }\n\n private cleanup() {\n if (this.ws) {\n this.ws.removeAllListeners();\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n }\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n }\n\n /**\n * Starts sending periodic ping messages to the server.\n *\n * This function uses `setInterval` to send a ping at the configured `pingInterval`.\n * If the WebSocket is not open, pings are not sent.\n */\n private startHeartbeat() {\n this.pingTimeout = setInterval(() => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.ping();\n }\n }, this.pingInterval);\n }\n\n /**\n * Stops sending heartbeat pings by clearing the ping interval.\n */\n private stopHeartbeat() {\n if (this.pingTimeout) {\n clearInterval(this.pingTimeout);\n this.pingTimeout = null;\n }\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n *\n * Override this method in a subclass to implement custom logic on connection.\n */\n protected onOpen() {\n // Custom logic for connection open\n }\n\n /**\n * Called when a WebSocket message is received.\n *\n * @param data - The data received from the WebSocket server.\n *\n * Override this method in a subclass to implement custom message handling.\n */\n protected onMessage(_data: WebSocket.Data) {\n // Custom logic for handling received messages\n }\n\n /**\n * Called when a WebSocket error occurs.\n *\n * @param error - The error that occurred.\n *\n * Override this method in a subclass to implement custom error handling.\n * Note: Service switching is now handled in the reconnection logic, not here.\n */\n protected onError(_error: Error) {\n // Custom logic for handling errors - override in subclasses\n // Service switching is handled automatically in scheduleReconnect()\n }\n\n /**\n * Called when the WebSocket connection is closed.\n *\n * Override this method in a subclass to implement custom logic on disconnection.\n */\n protected onClose() {\n // Custom logic for handling connection close\n }\n\n /**\n * Sends data to the connected WebSocket server, if the connection is open.\n *\n * @param data - The data to send.\n */\n public send(data: string | Buffer | ArrayBuffer | Buffer[]) {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data);\n }\n }\n\n /**\n * Closes the WebSocket connection gracefully.\n */\n public close() {\n this.shouldReconnect = false;\n this.stopHeartbeat();\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.ws) {\n this.ws.close();\n }\n\n // Unregister health check when closing\n healthMonitor.unregisterHealthCheck(this.healthCheckName);\n }\n\n public getConnectionState(): string {\n if (!this.ws) return \"DISCONNECTED\";\n\n switch (this.ws.readyState) {\n case WebSocket.CONNECTING:\n return \"CONNECTING\";\n case WebSocket.OPEN:\n return \"CONNECTED\";\n case WebSocket.CLOSING:\n return \"CLOSING\";\n case WebSocket.CLOSED:\n return \"DISCONNECTED\";\n default:\n return \"UNKNOWN\";\n }\n }\n\n public getReconnectAttempts(): number {\n return this.reconnectAttempts;\n }\n\n public getServiceCycles(): number {\n return this.serviceCycles;\n }\n\n public getServiceIndex(): number {\n return this.serviceIndex;\n }\n\n public getAllServices(): string[] {\n return Array.isArray(this.service) ? [...this.service] : [this.service];\n }\n\n public getCurrentService(): string {\n return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;\n }\n\n public getMessageCount(): number {\n return this.messageCount;\n }\n\n public getLastMessageTime(): number {\n return this.lastMessageTime;\n }\n\n public getHealthCheckName(): string {\n return this.healthCheckName;\n }\n}\n","import { Logger } from \"./logger\";\n\nexport interface HealthStatus {\n healthy: boolean;\n timestamp: number;\n checks: Record<string, boolean>;\n metrics: Record<string, number>;\n details?: Record<string, unknown>;\n}\n\nexport interface HealthCheckOptions {\n interval?: number; // milliseconds\n timeout?: number; // milliseconds\n retries?: number;\n}\n\n/**\n * Health monitoring system for bot components.\n * Provides health checks and basic metrics collection.\n */\nexport class HealthMonitor {\n private checks = new Map<string, () => Promise<boolean>>();\n private metrics = new Map<string, number>();\n private lastCheckResults = new Map<string, boolean>();\n private checkInterval: NodeJS.Timeout | null = null;\n private options: Required<HealthCheckOptions>;\n\n constructor(options: HealthCheckOptions = {}) {\n this.options = {\n interval: options.interval || 30000, // 30 seconds\n timeout: options.timeout || 5000, // 5 seconds\n retries: options.retries || 2,\n };\n }\n\n /**\n * Register a health check function.\n * @param name - Unique name for the health check\n * @param checkFn - Function that returns true if healthy\n */\n registerHealthCheck(name: string, checkFn: () => Promise<boolean>) {\n this.checks.set(name, checkFn);\n Logger.debug(`Registered health check: ${name}`);\n }\n\n /**\n * Remove a health check.\n * @param name - Name of the health check to remove\n */\n unregisterHealthCheck(name: string) {\n this.checks.delete(name);\n this.lastCheckResults.delete(name);\n Logger.debug(`Unregistered health check: ${name}`);\n }\n\n /**\n * Set a metric value.\n * @param name - Metric name\n * @param value - Metric value\n */\n setMetric(name: string, value: number) {\n this.metrics.set(name, value);\n }\n\n /**\n * Increment a counter metric.\n * @param name - Metric name\n * @param increment - Value to add (default: 1)\n */\n incrementMetric(name: string, increment = 1) {\n const current = this.metrics.get(name) || 0;\n this.metrics.set(name, current + increment);\n }\n\n /**\n * Get current metric value.\n * @param name - Metric name\n * @returns Current value or 0 if not found\n */\n getMetric(name: string): number {\n return this.metrics.get(name) || 0;\n }\n\n /**\n * Get all current metrics.\n * @returns Object with all metrics\n */\n getAllMetrics(): Record<string, number> {\n return Object.fromEntries(this.metrics);\n }\n\n /**\n * Run a single health check with timeout and retries.\n * @private\n */\n private async runHealthCheck(name: string, checkFn: () => Promise<boolean>): Promise<boolean> {\n for (let attempt = 0; attempt <= this.options.retries; attempt++) {\n try {\n const result = await this.withTimeout(checkFn(), this.options.timeout);\n if (result) {\n return true;\n }\n } catch (error) {\n Logger.debug(\n `Health check \"${name}\" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`,\n { error: error.message }\n );\n }\n }\n return false;\n }\n\n /**\n * Wrap a promise with a timeout.\n * @private\n */\n private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n return Promise.race([\n promise,\n new Promise<T>((_, reject) =>\n setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)\n ),\n ]);\n }\n\n /**\n * Run all health checks and return the current health status.\n */\n async getHealthStatus(): Promise<HealthStatus> {\n const timestamp = Date.now();\n const checkResults: Record<string, boolean> = {};\n const details: Record<string, unknown> = {};\n\n // Run all health checks\n const checkPromises = Array.from(this.checks.entries()).map(async ([name, checkFn]) => {\n const result = await this.runHealthCheck(name, checkFn);\n checkResults[name] = result;\n this.lastCheckResults.set(name, result);\n\n if (!result) {\n details[`${name}_last_failure`] = new Date().toISOString();\n }\n\n return result;\n });\n\n await Promise.allSettled(checkPromises);\n\n // Determine overall health\n const healthy = Object.values(checkResults).every(result => result);\n\n // Get current metrics\n const metrics = this.getAllMetrics();\n\n return {\n healthy,\n timestamp,\n checks: checkResults,\n metrics,\n details,\n };\n }\n\n /**\n * Start periodic health monitoring.\n */\n start() {\n if (this.checkInterval) {\n this.stop();\n }\n\n Logger.info(`Starting health monitor with ${this.options.interval}ms interval`);\n\n this.checkInterval = setInterval(async () => {\n try {\n const status = await this.getHealthStatus();\n\n if (!status.healthy) {\n const failedChecks = Object.entries(status.checks)\n .filter(([, healthy]) => !healthy)\n .map(([name]) => name);\n\n Logger.warn(`Health check failed`, {\n operation: \"health_check\",\n failed_checks: failedChecks,\n metrics: status.metrics,\n });\n } else {\n Logger.debug(\"Health check passed\", {\n operation: \"health_check\",\n metrics: status.metrics,\n });\n }\n } catch (error) {\n Logger.error(\"Error during health check:\", { error: error.message });\n }\n }, this.options.interval);\n }\n\n /**\n * Stop periodic health monitoring.\n */\n stop() {\n if (this.checkInterval) {\n clearInterval(this.checkInterval);\n this.checkInterval = null;\n Logger.info(\"Stopped health monitor\");\n }\n }\n\n /**\n * Get a summary of the last health check results.\n */\n getLastCheckSummary(): Record<string, boolean> {\n return Object.fromEntries(this.lastCheckResults);\n }\n}\n\n// Global health monitor instance\nexport const healthMonitor = new HealthMonitor();\n","import WebSocket from \"ws\";\nimport { WebSocketClient } from \"./websocketClient\";\nimport { Logger } from \"./logger\";\n\n/**\n * Represents a subscription to a Jetstream feed over WebSocket.\n *\n * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats.\n * It invokes a provided callback function whenever a message is received from the Jetstream server.\n */\nexport class JetstreamSubscription extends WebSocketClient {\n /**\n * Creates a new `JetstreamSubscription`.\n *\n * @param service - The URL(-Array) of the Jetstream server(s) to connect to.\n * @param interval - The interval (in milliseconds) for reconnect attempts.\n * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.\n */\n constructor(\n service: string | string[],\n public interval: number,\n private onMessageCallback?: (data: WebSocket.Data) => void\n ) {\n super({ service, reconnectInterval: interval });\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * Logs a message indicating that the connection to the Jetstream server has been established.\n */\n protected onOpen() {\n Logger.info(\"Connected to Jetstream server.\");\n super.onOpen();\n }\n\n /**\n * Called when a WebSocket message is received.\n *\n * If an `onMessageCallback` was provided, it is invoked with the received data.\n *\n * @param data - The data received from the Jetstream server.\n */\n protected onMessage(data: WebSocket.Data) {\n if (this.onMessageCallback) {\n this.onMessageCallback(data);\n }\n }\n\n /**\n * Called when a WebSocket error occurs.\n * Logs the error message indicating that Jetstream encountered an error.\n *\n * @param error - The error that occurred.\n */\n protected onError(error: Error) {\n Logger.error(\"Jetstream encountered an error:\", error);\n super.onError(error);\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * Logs a message indicating that the Jetstream connection has closed.\n */\n protected onClose() {\n Logger.info(\"Jetstream connection closed.\");\n super.onClose();\n }\n}\n","/**\n * Returns the given string if it is defined; otherwise returns `undefined`.\n *\n * @param val - The optional string value to check.\n * @returns The given string if defined, or `undefined` if `val` is falsy.\n */\nexport const maybeStr = (val?: string): string | undefined => {\n if (!val) return undefined;\n return val;\n};\n\n/**\n * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.\n *\n * @param val - The optional string value to parse.\n * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.\n */\nexport const maybeInt = (val?: string): number | undefined => {\n if (!val) return undefined;\n const int = parseInt(val, 10);\n if (isNaN(int)) return undefined;\n return int;\n};\n","import WebSocket from \"ws\";\nimport { Post } from \"../types/post\";\nimport { WebsocketMessage } from \"../types/message\";\n/**\n * Converts a raw WebSocket message into a `FeedEntry` object, if possible.\n *\n * This function checks if the incoming WebSocket data is structured like a feed commit message\n * with the required properties for a created post. If the data matches the expected shape,\n * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`.\n *\n * @param data - The raw WebSocket data.\n * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`.\n */\nexport function websocketToFeedEntry(data: WebSocket.Data): Post | null {\n const message = data as WebsocketMessage;\n if (\n !message.commit ||\n !message.commit.record ||\n !message.commit.record[\"$type\"] ||\n !message.did ||\n !message.commit.cid ||\n !message.commit.rkey ||\n message.commit.operation !== \"create\"\n ) {\n return null;\n }\n const messageUri = `at://${message.did}/${message.commit.record[\"$type\"]}/${message.commit.rkey}`;\n return {\n cid: message.commit.cid,\n uri: messageUri,\n authorDid: message.did,\n text: message.commit.record.text,\n rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,\n rootUri: message.commit.record.reply?.root.uri ?? messageUri,\n };\n}\n"],"mappings":"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","names":["LogLevel","import_api","import_api","WebSocket"]}
+584-58
dist/index.mjs
···11+var __defProp = Object.defineProperty;
22+var __getOwnPropSymbols = Object.getOwnPropertySymbols;
33+var __hasOwnProp = Object.prototype.hasOwnProperty;
44+var __propIsEnum = Object.prototype.propertyIsEnumerable;
55+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
66+var __spreadValues = (a, b) => {
77+ for (var prop in b || (b = {}))
88+ if (__hasOwnProp.call(b, prop))
99+ __defNormalProp(a, prop, b[prop]);
1010+ if (__getOwnPropSymbols)
1111+ for (var prop of __getOwnPropSymbols(b)) {
1212+ if (__propIsEnum.call(b, prop))
1313+ __defNormalProp(a, prop, b[prop]);
1414+ }
1515+ return a;
1616+};
117var __async = (__this, __arguments, generator) => {
218 return new Promise((resolve, reject) => {
319 var fulfilled = (value) => {
···2339import { AtpAgent } from "@atproto/api";
24402541// src/utils/logger.ts
4242+var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
4343+ LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
4444+ LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
4545+ LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
4646+ LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
4747+ return LogLevel2;
4848+})(LogLevel || {});
2649var Logger = class {
2750 /**
5151+ * Generate a new correlation ID for tracking related operations.
5252+ */
5353+ static generateCorrelationId() {
5454+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
5555+ }
5656+ /**
5757+ * Set the correlation ID for subsequent log entries.
5858+ * @param id - The correlation ID to use, or null to generate a new one
5959+ */
6060+ static setCorrelationId(id) {
6161+ this.correlationId = id || this.generateCorrelationId();
6262+ }
6363+ /**
6464+ * Get the current correlation ID.
6565+ */
6666+ static getCorrelationId() {
6767+ return this.correlationId;
6868+ }
6969+ /**
7070+ * Clear the current correlation ID.
7171+ */
7272+ static clearCorrelationId() {
7373+ this.correlationId = null;
7474+ }
7575+ /**
7676+ * Set the minimum log level. Messages below this level will not be logged.
7777+ * @param level - The minimum log level
7878+ */
7979+ static setLogLevel(level) {
8080+ this.logLevel = level;
8181+ }
8282+ /**
8383+ * Set the timezone for log timestamps.
8484+ * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC")
8585+ */
8686+ static setTimezone(timezone) {
8787+ this.timezone = timezone;
8888+ }
8989+ /**
9090+ * Get the current log level.
9191+ */
9292+ static getLogLevel() {
9393+ return this.logLevel;
9494+ }
9595+ /**
9696+ * Generate a formatted timestamp string.
9797+ * @private
9898+ */
9999+ static getTimestamp() {
100100+ return (/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: this.timezone });
101101+ }
102102+ /**
103103+ * Internal logging method that checks log level before processing.
104104+ * @private
105105+ */
106106+ static log(level, levelName, message, context, logFn = console.log) {
107107+ if (level < this.logLevel) {
108108+ return;
109109+ }
110110+ const timestamp = this.getTimestamp();
111111+ let formattedMessage = `${timestamp} [${levelName}]`;
112112+ if (this.correlationId) {
113113+ formattedMessage += ` [${this.correlationId}]`;
114114+ }
115115+ if (context && typeof context === "object" && "correlationId" in context && context.correlationId && context.correlationId !== this.correlationId) {
116116+ formattedMessage += ` [${context.correlationId}]`;
117117+ }
118118+ formattedMessage += `: ${message}`;
119119+ if (context) {
120120+ if (typeof context === "object") {
121121+ const logEntry = __spreadValues({
122122+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
123123+ level: levelName,
124124+ message,
125125+ correlationId: this.correlationId
126126+ }, context);
127127+ logFn(formattedMessage, logEntry);
128128+ } else {
129129+ logFn(formattedMessage, context);
130130+ }
131131+ } else {
132132+ logFn(formattedMessage);
133133+ }
134134+ }
135135+ /**
28136 * Logs an informational message to the console.
29137 *
30138 * @param message - The message to be logged.
3131- * @param context - Optional additional context (object or string) to log alongside the message.
139139+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
32140 */
33141 static info(message, context) {
3434- console.info(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [INFO]: ${message}`, context || "");
142142+ this.log(1 /* INFO */, "INFO", message, context, console.info);
35143 }
36144 /**
37145 * Logs a warning message to the console.
38146 *
39147 * @param message - The message to be logged.
4040- * @param context - Optional additional context (object or string) to log alongside the message.
148148+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
41149 */
42150 static warn(message, context) {
4343- console.warn(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [WARNING]: ${message}`, context || "");
151151+ this.log(2 /* WARN */, "WARNING", message, context, console.warn);
44152 }
45153 /**
46154 * Logs an error message to the console.
47155 *
48156 * @param message - The message to be logged.
4949- * @param context - Optional additional context (object or string) to log alongside the message.
157157+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
50158 */
51159 static error(message, context) {
5252- console.error(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [ERROR]: ${message}`, context || "");
160160+ this.log(3 /* ERROR */, "ERROR", message, context, console.error);
53161 }
54162 /**
55163 * Logs a debug message to the console.
56164 *
57165 * @param message - The message to be logged.
5858- * @param context - Optional additional context (object or string) to log alongside the message.
166166+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
59167 */
60168 static debug(message, context) {
6161- console.debug(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [DEBUG]: ${message}`, context || "");
169169+ this.log(0 /* DEBUG */, "DEBUG", message, context, console.debug);
170170+ }
171171+ /**
172172+ * Log operation start with timing.
173173+ * @param operation - The operation name
174174+ * @param context - Additional context
175175+ */
176176+ static startOperation(operation, context) {
177177+ const correlationId = (context == null ? void 0 : context.correlationId) || this.generateCorrelationId();
178178+ this.setCorrelationId(correlationId);
179179+ this.info(`Starting operation: ${operation}`, __spreadValues({
180180+ operation,
181181+ correlationId
182182+ }, context));
183183+ return correlationId;
184184+ }
185185+ /**
186186+ * Log operation completion with timing.
187187+ * @param operation - The operation name
188188+ * @param startTime - The start time from Date.now()
189189+ * @param context - Additional context
190190+ */
191191+ static endOperation(operation, startTime, context) {
192192+ const duration = Date.now() - startTime;
193193+ this.info(`Completed operation: ${operation}`, __spreadValues({
194194+ operation,
195195+ duration: `${duration}ms`
196196+ }, context));
62197 }
63198};
199199+Logger.logLevel = 1 /* INFO */;
200200+Logger.timezone = "Europe/Vienna";
201201+Logger.correlationId = null;
6420265203// src/bots/actionBot.ts
66204var ActionBotAgent = class extends AtpAgent {
···71209 }
72210 doAction(params) {
73211 return __async(this, null, function* () {
7474- this.actionBot.action(this, params);
212212+ const correlationId = Logger.startOperation("actionBot.doAction", {
213213+ botId: this.actionBot.username || this.actionBot.identifier
214214+ });
215215+ const startTime = Date.now();
216216+ try {
217217+ yield this.actionBot.action(this, params);
218218+ Logger.endOperation("actionBot.doAction", startTime, {
219219+ correlationId,
220220+ botId: this.actionBot.username || this.actionBot.identifier
221221+ });
222222+ } catch (error) {
223223+ Logger.error("Action bot execution failed", {
224224+ correlationId,
225225+ botId: this.actionBot.username || this.actionBot.identifier,
226226+ error: error instanceof Error ? error.message : String(error)
227227+ });
228228+ throw error;
229229+ }
75230 });
76231 }
77232};
78233var useActionBotAgent = (actionBot) => __async(void 0, null, function* () {
7979- var _a, _b, _c;
234234+ var _a;
235235+ const botId = (_a = actionBot.username) != null ? _a : actionBot.identifier;
236236+ const correlationId = Logger.startOperation("initializeActionBot", { botId });
237237+ const startTime = Date.now();
80238 const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);
81239 try {
8282- Logger.info(`Initialize action bot ${(_a = actionBot.username) != null ? _a : actionBot.identifier}`);
8383- const login = yield agent.login({ identifier: actionBot.identifier, password: actionBot.password });
240240+ Logger.info("Initializing action bot", { correlationId, botId });
241241+ const login = yield agent.login({
242242+ identifier: actionBot.identifier,
243243+ password: actionBot.password
244244+ });
84245 if (!login.success) {
8585- Logger.warn(`Failed to login action bot ${(_b = actionBot.username) != null ? _b : actionBot.identifier}`);
246246+ Logger.warn("Action bot login failed", { correlationId, botId });
86247 return null;
87248 }
249249+ Logger.endOperation("initializeActionBot", startTime, { correlationId, botId });
88250 return agent;
89251 } catch (error) {
9090- Logger.error("Failed to initialize action bot:", `${error}, ${(_c = actionBot.username) != null ? _c : actionBot.identifier}`);
252252+ Logger.error("Failed to initialize action bot", {
253253+ correlationId,
254254+ botId,
255255+ error: error.message,
256256+ duration: Date.now() - startTime
257257+ });
91258 return null;
92259 }
93260});
···116283 const agent = new CronBotAgent({ service: cronBot.service }, cronBot);
117284 try {
118285 Logger.info(`Initialize cron bot ${(_a = cronBot.username) != null ? _a : cronBot.identifier}`);
119119- const login = yield agent.login({ identifier: cronBot.identifier, password: cronBot.password });
286286+ const login = yield agent.login({
287287+ identifier: cronBot.identifier,
288288+ password: cronBot.password
289289+ });
120290 if (!login.success) {
121291 Logger.info(`Failed to login cron bot ${(_b = cronBot.username) != null ? _b : cronBot.identifier}`);
122292 return null;
···124294 agent.job.start();
125295 return agent;
126296 } catch (error) {
127127- Logger.error("Failed to initialize cron bot:", `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`);
297297+ Logger.error(
298298+ "Failed to initialize cron bot:",
299299+ `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`
300300+ );
128301 return null;
129302 }
130303});
···161334 message
162335 );
163336 yield Promise.all([this.like(post.uri, post.cid), this.post(reply)]);
164164- Logger.info(`Replied to post: ${post.uri}`, (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier);
337337+ Logger.info(
338338+ `Replied to post: ${post.uri}`,
339339+ (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier
340340+ );
165341 }
166342 } catch (error) {
167167- Logger.error("Error while replying:", `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`);
343343+ Logger.error(
344344+ "Error while replying:",
345345+ `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`
346346+ );
168347 }
169348 });
170349 }
···174353 $type: "app.bsky.feed.post",
175354 text: message,
176355 reply: {
177177- "root": root,
178178- "parent": parent
356356+ root,
357357+ parent
179358 }
180359 };
181360}
182361function filterBotReplies(text, botReplies) {
362362+ const lowerText = text.toLowerCase();
183363 return botReplies.filter((reply) => {
184364 const keyword = reply.keyword.toLowerCase();
185185- const keywordFound = text.toLowerCase().includes(keyword);
186186- if (!keywordFound) {
365365+ if (!lowerText.includes(keyword)) {
187366 return false;
188367 }
189189- if (Array.isArray(reply.exclude) && reply.exclude.length > 0) {
190190- for (const excludeWord of reply.exclude) {
191191- if (text.toLowerCase().includes(excludeWord.toLowerCase())) {
192192- return false;
193193- }
194194- }
368368+ if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) {
369369+ return true;
195370 }
196196- return true;
371371+ const hasExcludedWord = reply.exclude.some(
372372+ (excludeWord) => lowerText.includes(excludeWord.toLowerCase())
373373+ );
374374+ return !hasExcludedWord;
197375 });
198376}
199377var useKeywordBotAgent = (keywordBot) => __async(void 0, null, function* () {
200378 var _a, _b, _c;
201379 const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);
202380 try {
203203- const login = yield agent.login({ identifier: keywordBot.identifier, password: keywordBot.password });
381381+ const login = yield agent.login({
382382+ identifier: keywordBot.identifier,
383383+ password: keywordBot.password
384384+ });
204385 Logger.info(`Initialize keyword bot ${(_a = keywordBot.username) != null ? _a : keywordBot.identifier}`);
205386 if (!login.success) {
206387 Logger.warn(`Failed to login keyword bot ${(_b = keywordBot.username) != null ? _b : keywordBot.identifier}`);
···208389 }
209390 return agent;
210391 } catch (error) {
211211- Logger.error("Failed to initialize keyword bot:", `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`);
392392+ Logger.error(
393393+ "Failed to initialize keyword bot:",
394394+ `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`
395395+ );
212396 return null;
213397 }
214398});
215399216400// src/utils/websocketClient.ts
217401import WebSocket from "ws";
402402+403403+// src/utils/healthCheck.ts
404404+var HealthMonitor = class {
405405+ constructor(options = {}) {
406406+ this.checks = /* @__PURE__ */ new Map();
407407+ this.metrics = /* @__PURE__ */ new Map();
408408+ this.lastCheckResults = /* @__PURE__ */ new Map();
409409+ this.checkInterval = null;
410410+ this.options = {
411411+ interval: options.interval || 3e4,
412412+ // 30 seconds
413413+ timeout: options.timeout || 5e3,
414414+ // 5 seconds
415415+ retries: options.retries || 2
416416+ };
417417+ }
418418+ /**
419419+ * Register a health check function.
420420+ * @param name - Unique name for the health check
421421+ * @param checkFn - Function that returns true if healthy
422422+ */
423423+ registerHealthCheck(name, checkFn) {
424424+ this.checks.set(name, checkFn);
425425+ Logger.debug(`Registered health check: ${name}`);
426426+ }
427427+ /**
428428+ * Remove a health check.
429429+ * @param name - Name of the health check to remove
430430+ */
431431+ unregisterHealthCheck(name) {
432432+ this.checks.delete(name);
433433+ this.lastCheckResults.delete(name);
434434+ Logger.debug(`Unregistered health check: ${name}`);
435435+ }
436436+ /**
437437+ * Set a metric value.
438438+ * @param name - Metric name
439439+ * @param value - Metric value
440440+ */
441441+ setMetric(name, value) {
442442+ this.metrics.set(name, value);
443443+ }
444444+ /**
445445+ * Increment a counter metric.
446446+ * @param name - Metric name
447447+ * @param increment - Value to add (default: 1)
448448+ */
449449+ incrementMetric(name, increment = 1) {
450450+ const current = this.metrics.get(name) || 0;
451451+ this.metrics.set(name, current + increment);
452452+ }
453453+ /**
454454+ * Get current metric value.
455455+ * @param name - Metric name
456456+ * @returns Current value or 0 if not found
457457+ */
458458+ getMetric(name) {
459459+ return this.metrics.get(name) || 0;
460460+ }
461461+ /**
462462+ * Get all current metrics.
463463+ * @returns Object with all metrics
464464+ */
465465+ getAllMetrics() {
466466+ return Object.fromEntries(this.metrics);
467467+ }
468468+ /**
469469+ * Run a single health check with timeout and retries.
470470+ * @private
471471+ */
472472+ runHealthCheck(name, checkFn) {
473473+ return __async(this, null, function* () {
474474+ for (let attempt = 0; attempt <= this.options.retries; attempt++) {
475475+ try {
476476+ const result = yield this.withTimeout(checkFn(), this.options.timeout);
477477+ if (result) {
478478+ return true;
479479+ }
480480+ } catch (error) {
481481+ Logger.debug(
482482+ `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`,
483483+ { error: error.message }
484484+ );
485485+ }
486486+ }
487487+ return false;
488488+ });
489489+ }
490490+ /**
491491+ * Wrap a promise with a timeout.
492492+ * @private
493493+ */
494494+ withTimeout(promise, timeoutMs) {
495495+ return Promise.race([
496496+ promise,
497497+ new Promise(
498498+ (_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)
499499+ )
500500+ ]);
501501+ }
502502+ /**
503503+ * Run all health checks and return the current health status.
504504+ */
505505+ getHealthStatus() {
506506+ return __async(this, null, function* () {
507507+ const timestamp = Date.now();
508508+ const checkResults = {};
509509+ const details = {};
510510+ const checkPromises = Array.from(this.checks.entries()).map((_0) => __async(this, [_0], function* ([name, checkFn]) {
511511+ const result = yield this.runHealthCheck(name, checkFn);
512512+ checkResults[name] = result;
513513+ this.lastCheckResults.set(name, result);
514514+ if (!result) {
515515+ details[`${name}_last_failure`] = (/* @__PURE__ */ new Date()).toISOString();
516516+ }
517517+ return result;
518518+ }));
519519+ yield Promise.allSettled(checkPromises);
520520+ const healthy = Object.values(checkResults).every((result) => result);
521521+ const metrics = this.getAllMetrics();
522522+ return {
523523+ healthy,
524524+ timestamp,
525525+ checks: checkResults,
526526+ metrics,
527527+ details
528528+ };
529529+ });
530530+ }
531531+ /**
532532+ * Start periodic health monitoring.
533533+ */
534534+ start() {
535535+ if (this.checkInterval) {
536536+ this.stop();
537537+ }
538538+ Logger.info(`Starting health monitor with ${this.options.interval}ms interval`);
539539+ this.checkInterval = setInterval(() => __async(this, null, function* () {
540540+ try {
541541+ const status = yield this.getHealthStatus();
542542+ if (!status.healthy) {
543543+ const failedChecks = Object.entries(status.checks).filter(([, healthy]) => !healthy).map(([name]) => name);
544544+ Logger.warn(`Health check failed`, {
545545+ operation: "health_check",
546546+ failed_checks: failedChecks,
547547+ metrics: status.metrics
548548+ });
549549+ } else {
550550+ Logger.debug("Health check passed", {
551551+ operation: "health_check",
552552+ metrics: status.metrics
553553+ });
554554+ }
555555+ } catch (error) {
556556+ Logger.error("Error during health check:", { error: error.message });
557557+ }
558558+ }), this.options.interval);
559559+ }
560560+ /**
561561+ * Stop periodic health monitoring.
562562+ */
563563+ stop() {
564564+ if (this.checkInterval) {
565565+ clearInterval(this.checkInterval);
566566+ this.checkInterval = null;
567567+ Logger.info("Stopped health monitor");
568568+ }
569569+ }
570570+ /**
571571+ * Get a summary of the last health check results.
572572+ */
573573+ getLastCheckSummary() {
574574+ return Object.fromEntries(this.lastCheckResults);
575575+ }
576576+};
577577+var healthMonitor = new HealthMonitor();
578578+579579+// src/utils/websocketClient.ts
218580var WebSocketClient = class {
219581 /**
220582 * Creates a new instance of `WebSocketClient`.
221221- *
583583+ *
222584 * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.
223585 */
224586 constructor(options) {
225587 this.ws = null;
226588 this.pingTimeout = null;
227227- this.url = options.url;
589589+ this.serviceIndex = 0;
590590+ this.reconnectAttempts = 0;
591591+ this.serviceCycles = 0;
592592+ this.reconnectTimeout = null;
593593+ this.isConnecting = false;
594594+ this.shouldReconnect = true;
595595+ this.messageCount = 0;
596596+ this.lastMessageTime = 0;
597597+ this.service = options.service;
228598 this.reconnectInterval = options.reconnectInterval || 5e3;
229599 this.pingInterval = options.pingInterval || 1e4;
600600+ this.maxReconnectAttempts = options.maxReconnectAttempts || 3;
601601+ this.maxServiceCycles = options.maxServiceCycles || 2;
602602+ this.maxReconnectDelay = options.maxReconnectDelay || 3e4;
603603+ this.backoffFactor = options.backoffFactor || 1.5;
604604+ this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
605605+ healthMonitor.registerHealthCheck(this.healthCheckName, () => __async(this, null, function* () {
606606+ return this.getConnectionState() === "CONNECTED";
607607+ }));
608608+ healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0);
609609+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0);
230610 this.run();
231611 }
232612 /**
233613 * Initiates a WebSocket connection to the specified URL.
234234- *
614614+ *
235615 * This method sets up event listeners for `open`, `message`, `error`, and `close` events.
236616 * When the connection opens, it starts the heartbeat mechanism.
237617 * On close, it attempts to reconnect after a specified interval.
238618 */
239619 run() {
240240- this.ws = new WebSocket(this.url);
620620+ if (this.isConnecting) {
621621+ return;
622622+ }
623623+ this.isConnecting = true;
624624+ const currentService = Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;
625625+ Logger.info(`Attempting to connect to WebSocket: ${currentService}`);
626626+ this.ws = new WebSocket(currentService);
241627 this.ws.on("open", () => {
242242- Logger.info("WebSocket connected");
628628+ Logger.info("WebSocket connected successfully", {
629629+ service: this.getCurrentService(),
630630+ serviceIndex: this.serviceIndex
631631+ });
632632+ this.isConnecting = false;
633633+ this.reconnectAttempts = 0;
634634+ this.serviceCycles = 0;
635635+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
243636 this.startHeartbeat();
244637 this.onOpen();
245638 });
246639 this.ws.on("message", (data) => {
640640+ this.messageCount++;
641641+ this.lastMessageTime = Date.now();
642642+ healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`);
247643 this.onMessage(data);
248644 });
249645 this.ws.on("error", (error) => {
250646 Logger.error("WebSocket error:", error);
647647+ this.isConnecting = false;
251648 this.onError(error);
252649 });
253253- this.ws.on("close", () => {
254254- Logger.info("WebSocket disconnected");
650650+ this.ws.on("close", (code, reason) => {
651651+ Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`);
652652+ this.isConnecting = false;
255653 this.stopHeartbeat();
256654 this.onClose();
257257- this.reconnect();
655655+ if (this.shouldReconnect) {
656656+ this.scheduleReconnect();
657657+ }
258658 });
259659 }
260660 /**
261661 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
262662 * It clears all event listeners on the old WebSocket and initiates a new connection.
263663 */
264264- reconnect() {
664664+ scheduleReconnect() {
665665+ this.reconnectAttempts++;
666666+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
667667+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
668668+ if (this.shouldTryNextService()) {
669669+ this.moveToNextService();
670670+ return;
671671+ } else {
672672+ Logger.error("All services exhausted after maximum cycles", {
673673+ totalServices: Array.isArray(this.service) ? this.service.length : 1,
674674+ maxServiceCycles: this.maxServiceCycles,
675675+ serviceCycles: this.serviceCycles
676676+ });
677677+ return;
678678+ }
679679+ }
680680+ const delay = Math.min(
681681+ this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1),
682682+ this.maxReconnectDelay
683683+ );
684684+ Logger.info(
685685+ `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`,
686686+ {
687687+ service: this.getCurrentService(),
688688+ serviceIndex: this.serviceIndex,
689689+ delay: `${delay}ms`
690690+ }
691691+ );
692692+ if (this.reconnectTimeout) {
693693+ clearTimeout(this.reconnectTimeout);
694694+ }
695695+ this.reconnectTimeout = setTimeout(() => {
696696+ this.cleanup();
697697+ this.run();
698698+ }, delay);
699699+ }
700700+ /**
701701+ * Check if we should try the next service in the array.
702702+ */
703703+ shouldTryNextService() {
704704+ if (!Array.isArray(this.service)) {
705705+ return false;
706706+ }
707707+ return this.serviceCycles < this.maxServiceCycles;
708708+ }
709709+ /**
710710+ * Move to the next service in the array and reset reconnection attempts.
711711+ */
712712+ moveToNextService() {
713713+ if (!Array.isArray(this.service)) {
714714+ return;
715715+ }
716716+ const previousIndex = this.serviceIndex;
717717+ this.serviceIndex = (this.serviceIndex + 1) % this.service.length;
718718+ if (this.serviceIndex === 0) {
719719+ this.serviceCycles++;
720720+ }
721721+ this.reconnectAttempts = 0;
722722+ Logger.info("Switching to next service", {
723723+ previousService: this.service[previousIndex],
724724+ previousIndex,
725725+ newService: this.getCurrentService(),
726726+ newIndex: this.serviceIndex,
727727+ serviceCycle: this.serviceCycles
728728+ });
729729+ this.cleanup();
730730+ this.run();
731731+ }
732732+ cleanup() {
265733 if (this.ws) {
266734 this.ws.removeAllListeners();
735735+ if (this.ws.readyState === WebSocket.OPEN) {
736736+ this.ws.close();
737737+ }
267738 this.ws = null;
268739 }
269269- setTimeout(() => this.run(), this.reconnectInterval);
740740+ if (this.reconnectTimeout) {
741741+ clearTimeout(this.reconnectTimeout);
742742+ this.reconnectTimeout = null;
743743+ }
270744 }
271745 /**
272746 * Starts sending periodic ping messages to the server.
273273- *
747747+ *
274748 * This function uses `setInterval` to send a ping at the configured `pingInterval`.
275749 * If the WebSocket is not open, pings are not sent.
276750 */
···292766 }
293767 /**
294768 * Called when the WebSocket connection is successfully opened.
295295- *
769769+ *
296770 * Override this method in a subclass to implement custom logic on connection.
297771 */
298772 onOpen() {
299773 }
300774 /**
301775 * Called when a WebSocket message is received.
302302- *
776776+ *
303777 * @param data - The data received from the WebSocket server.
304304- *
778778+ *
305779 * Override this method in a subclass to implement custom message handling.
306780 */
307307- onMessage(data) {
781781+ onMessage(_data) {
308782 }
309783 /**
310784 * Called when a WebSocket error occurs.
311311- *
785785+ *
312786 * @param error - The error that occurred.
313313- *
787787+ *
314788 * Override this method in a subclass to implement custom error handling.
789789+ * Note: Service switching is now handled in the reconnection logic, not here.
315790 */
316316- onError(error) {
791791+ onError(_error) {
317792 }
318793 /**
319794 * Called when the WebSocket connection is closed.
320320- *
795795+ *
321796 * Override this method in a subclass to implement custom logic on disconnection.
322797 */
323798 onClose() {
324799 }
325800 /**
326801 * Sends data to the connected WebSocket server, if the connection is open.
327327- *
802802+ *
328803 * @param data - The data to send.
329804 */
330805 send(data) {
···336811 * Closes the WebSocket connection gracefully.
337812 */
338813 close() {
814814+ this.shouldReconnect = false;
815815+ this.stopHeartbeat();
816816+ if (this.reconnectTimeout) {
817817+ clearTimeout(this.reconnectTimeout);
818818+ this.reconnectTimeout = null;
819819+ }
339820 if (this.ws) {
340821 this.ws.close();
341822 }
823823+ healthMonitor.unregisterHealthCheck(this.healthCheckName);
824824+ }
825825+ getConnectionState() {
826826+ if (!this.ws) return "DISCONNECTED";
827827+ switch (this.ws.readyState) {
828828+ case WebSocket.CONNECTING:
829829+ return "CONNECTING";
830830+ case WebSocket.OPEN:
831831+ return "CONNECTED";
832832+ case WebSocket.CLOSING:
833833+ return "CLOSING";
834834+ case WebSocket.CLOSED:
835835+ return "DISCONNECTED";
836836+ default:
837837+ return "UNKNOWN";
838838+ }
839839+ }
840840+ getReconnectAttempts() {
841841+ return this.reconnectAttempts;
842842+ }
843843+ getServiceCycles() {
844844+ return this.serviceCycles;
845845+ }
846846+ getServiceIndex() {
847847+ return this.serviceIndex;
848848+ }
849849+ getAllServices() {
850850+ return Array.isArray(this.service) ? [...this.service] : [this.service];
851851+ }
852852+ getCurrentService() {
853853+ return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;
854854+ }
855855+ getMessageCount() {
856856+ return this.messageCount;
857857+ }
858858+ getLastMessageTime() {
859859+ return this.lastMessageTime;
860860+ }
861861+ getHealthCheckName() {
862862+ return this.healthCheckName;
342863 }
343864};
344865···346867var JetstreamSubscription = class extends WebSocketClient {
347868 /**
348869 * Creates a new `JetstreamSubscription`.
349349- *
350350- * @param service - The URL of the Jetstream server to connect to.
870870+ *
871871+ * @param service - The URL(-Array) of the Jetstream server(s) to connect to.
351872 * @param interval - The interval (in milliseconds) for reconnect attempts.
352873 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
353874 */
354875 constructor(service, interval, onMessageCallback) {
355355- super({ url: service, reconnectInterval: interval });
356356- this.service = service;
876876+ super({ service, reconnectInterval: interval });
357877 this.interval = interval;
358878 this.onMessageCallback = onMessageCallback;
359879 }
···363883 */
364884 onOpen() {
365885 Logger.info("Connected to Jetstream server.");
886886+ super.onOpen();
366887 }
367888 /**
368889 * Called when a WebSocket message is received.
369369- *
890890+ *
370891 * If an `onMessageCallback` was provided, it is invoked with the received data.
371371- *
892892+ *
372893 * @param data - The data received from the Jetstream server.
373894 */
374895 onMessage(data) {
···379900 /**
380901 * Called when a WebSocket error occurs.
381902 * Logs the error message indicating that Jetstream encountered an error.
382382- *
903903+ *
383904 * @param error - The error that occurred.
384905 */
385906 onError(error) {
386907 Logger.error("Jetstream encountered an error:", error);
908908+ super.onError(error);
387909 }
388910 /**
389911 * Called when the WebSocket connection is closed.
···391913 */
392914 onClose() {
393915 Logger.info("Jetstream connection closed.");
916916+ super.onClose();
394917 }
395918};
396919···426949export {
427950 ActionBotAgent,
428951 CronBotAgent,
952952+ HealthMonitor,
429953 JetstreamSubscription,
430954 KeywordBotAgent,
955955+ LogLevel,
431956 Logger,
432957 WebSocketClient,
433958 buildReplyToPost,
434959 filterBotReplies,
960960+ healthMonitor,
435961 maybeInt,
436962 maybeStr,
437963 useActionBotAgent,
+1-1
dist/index.mjs.map
···11-{"version":3,"sources":["../src/bots/actionBot.ts","../src/utils/logger.ts","../src/bots/cronBot.ts","../src/bots/keywordBot.ts","../src/utils/websocketClient.ts","../src/utils/jetstreamSubscription.ts","../src/utils/strings.ts","../src/utils/wsToFeed.ts"],"sourcesContent":["import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport { Logger } from '../utils/logger';\nimport type { ActionBot } from '../types/bot';\n\nexport class ActionBotAgent extends AtpAgent {\n constructor(public opts: AtpAgentOptions, public actionBot: ActionBot) {\n super(opts);\n }\n\n async doAction(params:any): Promise<void> {\n this.actionBot.action(this, params);\n }\n}\n\nexport const useActionBotAgent = async (actionBot: ActionBot): Promise<ActionBotAgent | null> => {\n const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);\n \n try {\n Logger.info(`Initialize action bot ${actionBot.username ?? actionBot.identifier}`);\n const login = await agent.login({ identifier: actionBot.identifier, password: actionBot.password! });\n if (!login.success) {\n Logger.warn(`Failed to login action bot ${actionBot.username ?? actionBot.identifier}`);\n return null;\n }\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize action bot:\", `${error}, ${actionBot.username ?? actionBot.identifier}`);\n return null;\n }\n};","/**\n * A simple logging utility class providing static methods for various log levels.\n * Each log message is prefixed with a timestamp and log level.\n */\nexport class Logger {\n /**\n * Logs an informational message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static info(message: string, context?: object | string) {\n console.info(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [INFO]: ${message}`, context || '');\n }\n\n /**\n * Logs a warning message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static warn(message: string, context?: object | string) {\n console.warn(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [WARNING]: ${message}`, context || '');\n }\n\n /**\n * Logs an error message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static error(message: string, context?: object | string) {\n console.error(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [ERROR]: ${message}`, context || '');\n }\n\n /**\n * Logs a debug message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (object or string) to log alongside the message.\n */\n static debug(message: string, context?: object | string) {\n console.debug(`${new Date().toLocaleString(\"de-DE\", {timeZone: \"Europe/Vienna\"})} [DEBUG]: ${message}`, context || '');\n }\n}","import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport { CronJob } from 'cron';\nimport { Logger } from '../utils/logger';\nimport type { CronBot } from '../types/bot';\n\nexport class CronBotAgent extends AtpAgent {\n public job: CronJob;\n\n constructor(public opts: AtpAgentOptions, public cronBot: CronBot) {\n super(opts);\n\n this.job = new CronJob(\n cronBot.cronJob.scheduleExpression,\n async () => cronBot.action(this),\n cronBot.cronJob.callback,\n false,\n cronBot.cronJob.timeZone,\n );\n }\n}\n\nexport const useCronBotAgent = async (cronBot: CronBot): Promise<CronBotAgent | null> => {\n const agent = new CronBotAgent({ service: cronBot.service }, cronBot);\n \n try {\n Logger.info(`Initialize cron bot ${cronBot.username ?? cronBot.identifier}`);\n const login = await agent.login({ identifier: cronBot.identifier, password: cronBot.password! });\n if (!login.success) {\n Logger.info(`Failed to login cron bot ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n agent.job.start();\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize cron bot:\", `${error}, ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n};","import { AtpAgent, AtpAgentOptions } from '@atproto/api';\nimport type { BotReply, KeywordBot } from '../types/bot';\nimport type { Post, UriCid } from \"../types/post\";\nimport { Logger } from '../utils/logger';\n\n\nexport class KeywordBotAgent extends AtpAgent {\n constructor(public opts: AtpAgentOptions, public keywordBot: KeywordBot) {\n super(opts);\n }\n \n async likeAndReplyIfFollower(post: Post): Promise<void> {\n if (post.authorDid === this.assertDid) {\n return;\n }\n\n const replies = filterBotReplies(post.text, this.keywordBot.replies);\n if (replies.length < 1) {\n return;\n }\n\n try {\n const actorProfile = await this.getProfile({actor: post.authorDid});\n\n if(actorProfile.success) {\n \n if (!actorProfile.data.viewer?.followedBy) {\n return;\n }\n\n const replyCfg = replies[Math.floor(Math.random() * replies.length)];\n const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)];\n const reply = buildReplyToPost(\n { uri: post.rootUri, cid: post.rootCid },\n { uri: post.uri, cid: post.cid },\n message\n );\n\n await Promise.all([this.like(post.uri, post.cid), this.post(reply)]);\n Logger.info(`Replied to post: ${post.uri}`, this.keywordBot.username ?? this.keywordBot.identifier);\n }\n } catch (error) {\n Logger.error(\"Error while replying:\", `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}`);\n }\n }\n}\n\nexport function buildReplyToPost (root: UriCid, parent: UriCid, message: string) { \n return {\n $type: \"app.bsky.feed.post\" as \"app.bsky.feed.post\",\n text: message,\n reply: {\n \"root\": root,\n \"parent\": parent\n }\n };\n}\n\nexport function filterBotReplies(text: string, botReplies: BotReply[]) {\n return botReplies.filter(reply => {\n const keyword = reply.keyword.toLowerCase();\n const keywordFound = text.toLowerCase().includes(keyword);\n if (!keywordFound) {\n return false;\n }\n\n if (Array.isArray(reply.exclude) && reply.exclude.length > 0) {\n for (const excludeWord of reply.exclude) {\n if (text.toLowerCase().includes(excludeWord.toLowerCase())) {\n return false;\n }\n }\n }\n\n return true;\n });\n}\n\nexport const useKeywordBotAgent = async (keywordBot: KeywordBot): Promise<KeywordBotAgent | null> => {\n const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);\n\n try {\n const login = await agent.login({ identifier: keywordBot.identifier, password: keywordBot.password! });\n\n Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n\n if (!login.success) { \n Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize keyword bot:\", `${error}, ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n};","import WebSocket from 'ws';\nimport { Logger } from './logger';\n\ninterface WebSocketClientOptions {\n /** The URL of the WebSocket server to connect to. */\n url: string;\n /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */\n reconnectInterval?: number;\n /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */\n pingInterval?: number;\n}\n\n/**\n * A WebSocket client that automatically attempts to reconnect upon disconnection\n * and periodically sends ping messages (heartbeats) to ensure the connection remains alive.\n * \n * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods\n * to implement custom handling of WebSocket events.\n */\nexport class WebSocketClient {\n private url: string;\n private reconnectInterval: number;\n private pingInterval: number;\n private ws: WebSocket | null = null;\n private pingTimeout: NodeJS.Timeout | null = null;\n\n /**\n * Creates a new instance of `WebSocketClient`.\n * \n * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.\n */\n constructor(options: WebSocketClientOptions) {\n this.url = options.url;\n this.reconnectInterval = options.reconnectInterval || 5000;\n this.pingInterval = options.pingInterval || 10000; \n this.run();\n }\n\n /**\n * Initiates a WebSocket connection to the specified URL.\n * \n * This method sets up event listeners for `open`, `message`, `error`, and `close` events.\n * When the connection opens, it starts the heartbeat mechanism.\n * On close, it attempts to reconnect after a specified interval.\n */\n private run() {\n this.ws = new WebSocket(this.url);\n\n this.ws.on('open', () => {\n Logger.info('WebSocket connected');\n this.startHeartbeat();\n this.onOpen();\n });\n\n this.ws.on('message', (data: WebSocket.Data) => {\n this.onMessage(data);\n });\n\n this.ws.on('error', (error) => {\n Logger.error('WebSocket error:', error);\n this.onError(error);\n });\n\n this.ws.on('close', () => {\n Logger.info('WebSocket disconnected');\n this.stopHeartbeat();\n this.onClose();\n this.reconnect();\n });\n }\n\n /**\n * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.\n * It clears all event listeners on the old WebSocket and initiates a new connection.\n */\n private reconnect() {\n if (this.ws) {\n this.ws.removeAllListeners();\n this.ws = null;\n }\n\n setTimeout(() => this.run(), this.reconnectInterval);\n }\n\n /**\n * Starts sending periodic ping messages to the server.\n * \n * This function uses `setInterval` to send a ping at the configured `pingInterval`.\n * If the WebSocket is not open, pings are not sent.\n */\n private startHeartbeat() {\n this.pingTimeout = setInterval(() => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.ping(); \n }\n }, this.pingInterval);\n }\n\n /**\n * Stops sending heartbeat pings by clearing the ping interval.\n */\n private stopHeartbeat() {\n if (this.pingTimeout) {\n clearInterval(this.pingTimeout);\n this.pingTimeout = null;\n }\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * \n * Override this method in a subclass to implement custom logic on connection.\n */\n protected onOpen() {\n // Custom logic for connection open\n }\n\n /**\n * Called when a WebSocket message is received.\n * \n * @param data - The data received from the WebSocket server.\n * \n * Override this method in a subclass to implement custom message handling.\n */\n protected onMessage(data: WebSocket.Data) {\n // Custom logic for handling received messages\n }\n\n /**\n * Called when a WebSocket error occurs.\n * \n * @param error - The error that occurred.\n * \n * Override this method in a subclass to implement custom error handling.\n */\n protected onError(error: Error) {\n // Custom logic for handling errors\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * \n * Override this method in a subclass to implement custom logic on disconnection.\n */\n protected onClose() {\n // Custom logic for handling connection close\n }\n\n /**\n * Sends data to the connected WebSocket server, if the connection is open.\n * \n * @param data - The data to send.\n */\n public send(data: any) {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data);\n }\n }\n\n /**\n * Closes the WebSocket connection gracefully.\n */\n public close() {\n if (this.ws) {\n this.ws.close();\n }\n }\n}","import WebSocket from 'ws';\nimport { WebSocketClient } from './websocketClient';\nimport { Logger } from './logger';\n\n/**\n * Represents a subscription to a Jetstream feed over WebSocket.\n * \n * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats.\n * It invokes a provided callback function whenever a message is received from the Jetstream server.\n */\nexport class JetstreamSubscription extends WebSocketClient {\n /**\n * Creates a new `JetstreamSubscription`.\n * \n * @param service - The URL of the Jetstream server to connect to.\n * @param interval - The interval (in milliseconds) for reconnect attempts.\n * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.\n */\n constructor(\n public service: string,\n public interval: number,\n private onMessageCallback?: (data: WebSocket.Data) => void\n ) {\n super({url: service, reconnectInterval: interval});\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * Logs a message indicating that the connection to the Jetstream server has been established.\n */\n protected onOpen() {\n Logger.info('Connected to Jetstream server.');\n }\n\n /**\n * Called when a WebSocket message is received.\n * \n * If an `onMessageCallback` was provided, it is invoked with the received data.\n * \n * @param data - The data received from the Jetstream server.\n */\n protected onMessage(data: WebSocket.Data) {\n if (this.onMessageCallback) {\n this.onMessageCallback(data);\n }\n }\n\n /**\n * Called when a WebSocket error occurs.\n * Logs the error message indicating that Jetstream encountered an error.\n * \n * @param error - The error that occurred.\n */\n protected onError(error: Error) {\n Logger.error('Jetstream encountered an error:', error);\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * Logs a message indicating that the Jetstream connection has closed.\n */\n protected onClose() {\n Logger.info('Jetstream connection closed.');\n }\n}\n","/**\n * Returns the given string if it is defined; otherwise returns `undefined`.\n * \n * @param val - The optional string value to check.\n * @returns The given string if defined, or `undefined` if `val` is falsy.\n */\nexport const maybeStr = (val?: string): string | undefined => {\n if (!val) return undefined;\n return val;\n}\n\n/**\n* Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.\n* \n* @param val - The optional string value to parse.\n* @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.\n*/\nexport const maybeInt = (val?: string): number | undefined => {\n if (!val) return undefined;\n const int = parseInt(val, 10);\n if (isNaN(int)) return undefined;\n return int;\n}","import WebSocket from 'ws';\nimport { Post } from \"../types/post\";\nimport { WebsocketMessage } from '../types/message';\n;\n\n/**\n * Converts a raw WebSocket message into a `FeedEntry` object, if possible.\n * \n * This function checks if the incoming WebSocket data is structured like a feed commit message\n * with the required properties for a created post. If the data matches the expected shape,\n * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`.\n * \n * @param data - The raw WebSocket data.\n * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`.\n */\nexport function websocketToFeedEntry(data: WebSocket.Data): Post | null {\n const message = data as WebsocketMessage;\n if(!message.commit || !message.commit.record || !message.commit.record['$type'] || !message.did || !message.commit.cid || !message.commit.rkey || message.commit.operation !== \"create\") {\n return null;\n }\n const messageUri = `at://${message.did}/${message.commit.record['$type']}/${message.commit.rkey}`;\n return {\n cid: message.commit.cid,\n uri: messageUri,\n authorDid: message.did,\n text: message.commit.record.text,\n rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,\n rootUri: message.commit.record.reply?.root.uri ?? messageUri,\n };\n}"],"mappings":"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","names":["AtpAgent","AtpAgent","AtpAgent","AtpAgent"]}11+{"version":3,"sources":["../src/bots/actionBot.ts","../src/utils/logger.ts","../src/bots/cronBot.ts","../src/bots/keywordBot.ts","../src/utils/websocketClient.ts","../src/utils/healthCheck.ts","../src/utils/jetstreamSubscription.ts","../src/utils/strings.ts","../src/utils/wsToFeed.ts"],"sourcesContent":["import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport { Logger } from \"../utils/logger\";\nimport type { ActionBot } from \"../types/bot\";\n\nexport class ActionBotAgent extends AtpAgent {\n constructor(\n public opts: AtpAgentOptions,\n public actionBot: ActionBot\n ) {\n super(opts);\n }\n\n async doAction(params?: unknown): Promise<void> {\n const correlationId = Logger.startOperation(\"actionBot.doAction\", {\n botId: this.actionBot.username || this.actionBot.identifier,\n });\n\n const startTime = Date.now();\n\n try {\n await this.actionBot.action(this, params);\n Logger.endOperation(\"actionBot.doAction\", startTime, {\n correlationId,\n botId: this.actionBot.username || this.actionBot.identifier,\n });\n } catch (error) {\n Logger.error(\"Action bot execution failed\", {\n correlationId,\n botId: this.actionBot.username || this.actionBot.identifier,\n error: error instanceof Error ? error.message : String(error),\n });\n throw error;\n }\n }\n}\n\nexport const useActionBotAgent = async (actionBot: ActionBot): Promise<ActionBotAgent | null> => {\n const botId = actionBot.username ?? actionBot.identifier;\n const correlationId = Logger.startOperation(\"initializeActionBot\", { botId });\n const startTime = Date.now();\n\n const agent = new ActionBotAgent({ service: actionBot.service }, actionBot);\n\n try {\n Logger.info(\"Initializing action bot\", { correlationId, botId });\n\n const login = await agent.login({\n identifier: actionBot.identifier,\n password: actionBot.password!,\n });\n\n if (!login.success) {\n Logger.warn(\"Action bot login failed\", { correlationId, botId });\n return null;\n }\n\n Logger.endOperation(\"initializeActionBot\", startTime, { correlationId, botId });\n return agent;\n } catch (error) {\n Logger.error(\"Failed to initialize action bot\", {\n correlationId,\n botId,\n error: error.message,\n duration: Date.now() - startTime,\n });\n return null;\n }\n};\n","export enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n}\n\nexport interface LogContext {\n correlationId?: string;\n botId?: string;\n operation?: string;\n duration?: number;\n [key: string]: unknown;\n}\n\n/**\n * A performance-optimized logging utility class providing static methods for various log levels.\n * Each log message is prefixed with a timestamp and log level.\n * Supports conditional logging based on log levels and configurable timezone.\n */\nexport class Logger {\n private static logLevel: LogLevel = LogLevel.INFO;\n private static timezone: string = \"Europe/Vienna\";\n private static correlationId: string | null = null;\n\n /**\n * Generate a new correlation ID for tracking related operations.\n */\n static generateCorrelationId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Set the correlation ID for subsequent log entries.\n * @param id - The correlation ID to use, or null to generate a new one\n */\n static setCorrelationId(id?: string | null) {\n this.correlationId = id || this.generateCorrelationId();\n }\n\n /**\n * Get the current correlation ID.\n */\n static getCorrelationId(): string | null {\n return this.correlationId;\n }\n\n /**\n * Clear the current correlation ID.\n */\n static clearCorrelationId() {\n this.correlationId = null;\n }\n\n /**\n * Set the minimum log level. Messages below this level will not be logged.\n * @param level - The minimum log level\n */\n static setLogLevel(level: LogLevel) {\n this.logLevel = level;\n }\n\n /**\n * Set the timezone for log timestamps.\n * @param timezone - The timezone string (e.g., \"Europe/Vienna\", \"UTC\")\n */\n static setTimezone(timezone: string) {\n this.timezone = timezone;\n }\n\n /**\n * Get the current log level.\n */\n static getLogLevel(): LogLevel {\n return this.logLevel;\n }\n\n /**\n * Generate a formatted timestamp string.\n * @private\n */\n private static getTimestamp(): string {\n return new Date().toLocaleString(\"de-DE\", { timeZone: this.timezone });\n }\n\n /**\n * Internal logging method that checks log level before processing.\n * @private\n */\n private static log(\n level: LogLevel,\n levelName: string,\n message: string,\n context?: LogContext | object | string,\n logFn = console.log\n ) {\n if (level < this.logLevel) {\n return; // Skip logging if below threshold\n }\n\n const timestamp = this.getTimestamp();\n let formattedMessage = `${timestamp} [${levelName}]`;\n\n // Add correlation ID if available\n if (this.correlationId) {\n formattedMessage += ` [${this.correlationId}]`;\n }\n\n // Add context correlation ID if provided and different from global one\n if (\n context &&\n typeof context === \"object\" &&\n \"correlationId\" in context &&\n context.correlationId &&\n context.correlationId !== this.correlationId\n ) {\n formattedMessage += ` [${context.correlationId}]`;\n }\n\n formattedMessage += `: ${message}`;\n\n if (context) {\n // Create structured log entry for objects\n if (typeof context === \"object\") {\n const logEntry = {\n timestamp: new Date().toISOString(),\n level: levelName,\n message,\n correlationId: this.correlationId,\n ...context,\n };\n logFn(formattedMessage, logEntry);\n } else {\n logFn(formattedMessage, context);\n }\n } else {\n logFn(formattedMessage);\n }\n }\n /**\n * Logs an informational message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static info(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.INFO, \"INFO\", message, context, console.info);\n }\n\n /**\n * Logs a warning message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static warn(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.WARN, \"WARNING\", message, context, console.warn);\n }\n\n /**\n * Logs an error message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static error(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.ERROR, \"ERROR\", message, context, console.error);\n }\n\n /**\n * Logs a debug message to the console.\n *\n * @param message - The message to be logged.\n * @param context - Optional additional context (LogContext, object or string) to log alongside the message.\n */\n static debug(message: string, context?: LogContext | object | string) {\n this.log(LogLevel.DEBUG, \"DEBUG\", message, context, console.debug);\n }\n\n /**\n * Log operation start with timing.\n * @param operation - The operation name\n * @param context - Additional context\n */\n static startOperation(operation: string, context?: LogContext): string {\n const correlationId = context?.correlationId || this.generateCorrelationId();\n this.setCorrelationId(correlationId);\n\n this.info(`Starting operation: ${operation}`, {\n operation,\n correlationId,\n ...context,\n });\n\n return correlationId;\n }\n\n /**\n * Log operation completion with timing.\n * @param operation - The operation name\n * @param startTime - The start time from Date.now()\n * @param context - Additional context\n */\n static endOperation(operation: string, startTime: number, context?: LogContext) {\n const duration = Date.now() - startTime;\n\n this.info(`Completed operation: ${operation}`, {\n operation,\n duration: `${duration}ms`,\n ...context,\n });\n }\n}\n","import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport { CronJob } from \"cron\";\nimport { Logger } from \"../utils/logger\";\nimport type { CronBot } from \"../types/bot\";\n\nexport class CronBotAgent extends AtpAgent {\n public job: CronJob;\n\n constructor(\n public opts: AtpAgentOptions,\n public cronBot: CronBot\n ) {\n super(opts);\n\n this.job = new CronJob(\n cronBot.cronJob.scheduleExpression,\n async () => cronBot.action(this),\n cronBot.cronJob.callback,\n false,\n cronBot.cronJob.timeZone\n );\n }\n}\n\nexport const useCronBotAgent = async (cronBot: CronBot): Promise<CronBotAgent | null> => {\n const agent = new CronBotAgent({ service: cronBot.service }, cronBot);\n\n try {\n Logger.info(`Initialize cron bot ${cronBot.username ?? cronBot.identifier}`);\n const login = await agent.login({\n identifier: cronBot.identifier,\n password: cronBot.password!,\n });\n if (!login.success) {\n Logger.info(`Failed to login cron bot ${cronBot.username ?? cronBot.identifier}`);\n return null;\n }\n agent.job.start();\n return agent;\n } catch (error) {\n Logger.error(\n \"Failed to initialize cron bot:\",\n `${error}, ${cronBot.username ?? cronBot.identifier}`\n );\n return null;\n }\n};\n","import { AtpAgent, AtpAgentOptions } from \"@atproto/api\";\nimport type { BotReply, KeywordBot } from \"../types/bot\";\nimport type { Post, UriCid } from \"../types/post\";\nimport { Logger } from \"../utils/logger\";\n\nexport class KeywordBotAgent extends AtpAgent {\n constructor(\n public opts: AtpAgentOptions,\n public keywordBot: KeywordBot\n ) {\n super(opts);\n }\n\n async likeAndReplyIfFollower(post: Post): Promise<void> {\n if (post.authorDid === this.assertDid) {\n return;\n }\n\n const replies = filterBotReplies(post.text, this.keywordBot.replies);\n if (replies.length < 1) {\n return;\n }\n\n try {\n const actorProfile = await this.getProfile({ actor: post.authorDid });\n\n if (actorProfile.success) {\n if (!actorProfile.data.viewer?.followedBy) {\n return;\n }\n\n const replyCfg = replies[Math.floor(Math.random() * replies.length)];\n const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)];\n const reply = buildReplyToPost(\n { uri: post.rootUri, cid: post.rootCid },\n { uri: post.uri, cid: post.cid },\n message\n );\n\n await Promise.all([this.like(post.uri, post.cid), this.post(reply)]);\n Logger.info(\n `Replied to post: ${post.uri}`,\n this.keywordBot.username ?? this.keywordBot.identifier\n );\n }\n } catch (error) {\n Logger.error(\n \"Error while replying:\",\n `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}`\n );\n }\n }\n}\n\nexport function buildReplyToPost(root: UriCid, parent: UriCid, message: string) {\n return {\n $type: \"app.bsky.feed.post\" as const,\n text: message,\n reply: {\n root: root,\n parent: parent,\n },\n };\n}\n\nexport function filterBotReplies(text: string, botReplies: BotReply[]) {\n // Cache the lowercased text to avoid multiple toLowerCase() calls\n const lowerText = text.toLowerCase();\n\n return botReplies.filter(reply => {\n // Use cached lowercase comparison\n const keyword = reply.keyword.toLowerCase();\n if (!lowerText.includes(keyword)) {\n return false;\n }\n\n // Early return if no exclusions\n if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) {\n return true;\n }\n\n // Use some() for early exit on first match\n const hasExcludedWord = reply.exclude.some(excludeWord =>\n lowerText.includes(excludeWord.toLowerCase())\n );\n\n return !hasExcludedWord;\n });\n}\n\nexport const useKeywordBotAgent = async (\n keywordBot: KeywordBot\n): Promise<KeywordBotAgent | null> => {\n const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot);\n\n try {\n const login = await agent.login({\n identifier: keywordBot.identifier,\n password: keywordBot.password!,\n });\n\n Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n\n if (!login.success) {\n Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`);\n return null;\n }\n\n return agent;\n } catch (error) {\n Logger.error(\n \"Failed to initialize keyword bot:\",\n `${error}, ${keywordBot.username ?? keywordBot.identifier}`\n );\n return null;\n }\n};\n","import WebSocket from \"ws\";\nimport { Logger } from \"./logger\";\nimport { healthMonitor } from \"./healthCheck\";\n\ninterface WebSocketClientOptions {\n /** The URL of the WebSocket server to connect to. */\n service: string | string[];\n /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */\n reconnectInterval?: number;\n /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */\n pingInterval?: number;\n /** Maximum number of consecutive reconnection attempts per service. Default is 3. */\n maxReconnectAttempts?: number;\n /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */\n maxReconnectDelay?: number;\n /** Exponential backoff factor for reconnection delays. Default is 1.5. */\n backoffFactor?: number;\n /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */\n maxServiceCycles?: number;\n}\n\n/**\n * A WebSocket client that automatically attempts to reconnect upon disconnection\n * and periodically sends ping messages (heartbeats) to ensure the connection remains alive.\n *\n * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods\n * to implement custom handling of WebSocket events.\n */\nexport class WebSocketClient {\n private service: string | string[];\n private reconnectInterval: number;\n private pingInterval: number;\n private ws: WebSocket | null = null;\n private pingTimeout: NodeJS.Timeout | null = null;\n private serviceIndex = 0;\n private reconnectAttempts = 0;\n private serviceCycles = 0;\n private maxReconnectAttempts: number;\n private maxServiceCycles: number;\n private maxReconnectDelay: number;\n private backoffFactor: number;\n private reconnectTimeout: NodeJS.Timeout | null = null;\n private isConnecting = false;\n private shouldReconnect = true;\n private messageCount = 0;\n private lastMessageTime = 0;\n private healthCheckName: string;\n\n /**\n * Creates a new instance of `WebSocketClient`.\n *\n * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.\n */\n constructor(options: WebSocketClientOptions) {\n this.service = options.service;\n this.reconnectInterval = options.reconnectInterval || 5000;\n this.pingInterval = options.pingInterval || 10000;\n this.maxReconnectAttempts = options.maxReconnectAttempts || 3;\n this.maxServiceCycles = options.maxServiceCycles || 2;\n this.maxReconnectDelay = options.maxReconnectDelay || 30000;\n this.backoffFactor = options.backoffFactor || 1.5;\n\n // Generate unique health check name\n this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;\n\n // Register health check\n healthMonitor.registerHealthCheck(this.healthCheckName, async () => {\n return this.getConnectionState() === \"CONNECTED\";\n });\n\n // Initialize metrics\n healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0);\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0);\n\n this.run();\n }\n\n /**\n * Initiates a WebSocket connection to the specified URL.\n *\n * This method sets up event listeners for `open`, `message`, `error`, and `close` events.\n * When the connection opens, it starts the heartbeat mechanism.\n * On close, it attempts to reconnect after a specified interval.\n */\n private run() {\n if (this.isConnecting) {\n return;\n }\n\n this.isConnecting = true;\n const currentService = Array.isArray(this.service)\n ? this.service[this.serviceIndex]\n : this.service;\n\n Logger.info(`Attempting to connect to WebSocket: ${currentService}`);\n this.ws = new WebSocket(currentService);\n\n this.ws.on(\"open\", () => {\n Logger.info(\"WebSocket connected successfully\", {\n service: this.getCurrentService(),\n serviceIndex: this.serviceIndex,\n });\n this.isConnecting = false;\n this.reconnectAttempts = 0; // Reset on successful connection\n this.serviceCycles = 0; // Reset cycles on successful connection\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);\n this.startHeartbeat();\n this.onOpen();\n });\n\n this.ws.on(\"message\", (data: WebSocket.Data) => {\n this.messageCount++;\n this.lastMessageTime = Date.now();\n healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`);\n this.onMessage(data);\n });\n\n this.ws.on(\"error\", error => {\n Logger.error(\"WebSocket error:\", error);\n this.isConnecting = false;\n this.onError(error);\n });\n\n this.ws.on(\"close\", (code, reason) => {\n Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`);\n this.isConnecting = false;\n this.stopHeartbeat();\n this.onClose();\n\n if (this.shouldReconnect) {\n this.scheduleReconnect();\n }\n });\n }\n\n /**\n * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.\n * It clears all event listeners on the old WebSocket and initiates a new connection.\n */\n private scheduleReconnect() {\n this.reconnectAttempts++;\n healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);\n\n // Check if we should try the next service\n if (this.reconnectAttempts >= this.maxReconnectAttempts) {\n if (this.shouldTryNextService()) {\n this.moveToNextService();\n return; // Try next service immediately\n } else {\n Logger.error(\"All services exhausted after maximum cycles\", {\n totalServices: Array.isArray(this.service) ? this.service.length : 1,\n maxServiceCycles: this.maxServiceCycles,\n serviceCycles: this.serviceCycles,\n });\n return; // Give up entirely\n }\n }\n\n const delay = Math.min(\n this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1),\n this.maxReconnectDelay\n );\n\n Logger.info(\n `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`,\n {\n service: this.getCurrentService(),\n serviceIndex: this.serviceIndex,\n delay: `${delay}ms`,\n }\n );\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n }\n\n this.reconnectTimeout = setTimeout(() => {\n this.cleanup();\n this.run();\n }, delay);\n }\n\n /**\n * Check if we should try the next service in the array.\n */\n private shouldTryNextService(): boolean {\n if (!Array.isArray(this.service)) {\n return false; // Single service, can't switch\n }\n\n return this.serviceCycles < this.maxServiceCycles;\n }\n\n /**\n * Move to the next service in the array and reset reconnection attempts.\n */\n private moveToNextService() {\n if (!Array.isArray(this.service)) {\n return;\n }\n\n const previousIndex = this.serviceIndex;\n this.serviceIndex = (this.serviceIndex + 1) % this.service.length;\n\n // If we've gone through all services once, increment the cycle counter\n if (this.serviceIndex === 0) {\n this.serviceCycles++;\n }\n\n this.reconnectAttempts = 0; // Reset attempts for the new service\n\n Logger.info(\"Switching to next service\", {\n previousService: this.service[previousIndex],\n previousIndex,\n newService: this.getCurrentService(),\n newIndex: this.serviceIndex,\n serviceCycle: this.serviceCycles,\n });\n\n // Try the new service immediately\n this.cleanup();\n this.run();\n }\n\n private cleanup() {\n if (this.ws) {\n this.ws.removeAllListeners();\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.close();\n }\n this.ws = null;\n }\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n }\n\n /**\n * Starts sending periodic ping messages to the server.\n *\n * This function uses `setInterval` to send a ping at the configured `pingInterval`.\n * If the WebSocket is not open, pings are not sent.\n */\n private startHeartbeat() {\n this.pingTimeout = setInterval(() => {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.ping();\n }\n }, this.pingInterval);\n }\n\n /**\n * Stops sending heartbeat pings by clearing the ping interval.\n */\n private stopHeartbeat() {\n if (this.pingTimeout) {\n clearInterval(this.pingTimeout);\n this.pingTimeout = null;\n }\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n *\n * Override this method in a subclass to implement custom logic on connection.\n */\n protected onOpen() {\n // Custom logic for connection open\n }\n\n /**\n * Called when a WebSocket message is received.\n *\n * @param data - The data received from the WebSocket server.\n *\n * Override this method in a subclass to implement custom message handling.\n */\n protected onMessage(_data: WebSocket.Data) {\n // Custom logic for handling received messages\n }\n\n /**\n * Called when a WebSocket error occurs.\n *\n * @param error - The error that occurred.\n *\n * Override this method in a subclass to implement custom error handling.\n * Note: Service switching is now handled in the reconnection logic, not here.\n */\n protected onError(_error: Error) {\n // Custom logic for handling errors - override in subclasses\n // Service switching is handled automatically in scheduleReconnect()\n }\n\n /**\n * Called when the WebSocket connection is closed.\n *\n * Override this method in a subclass to implement custom logic on disconnection.\n */\n protected onClose() {\n // Custom logic for handling connection close\n }\n\n /**\n * Sends data to the connected WebSocket server, if the connection is open.\n *\n * @param data - The data to send.\n */\n public send(data: string | Buffer | ArrayBuffer | Buffer[]) {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(data);\n }\n }\n\n /**\n * Closes the WebSocket connection gracefully.\n */\n public close() {\n this.shouldReconnect = false;\n this.stopHeartbeat();\n\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.ws) {\n this.ws.close();\n }\n\n // Unregister health check when closing\n healthMonitor.unregisterHealthCheck(this.healthCheckName);\n }\n\n public getConnectionState(): string {\n if (!this.ws) return \"DISCONNECTED\";\n\n switch (this.ws.readyState) {\n case WebSocket.CONNECTING:\n return \"CONNECTING\";\n case WebSocket.OPEN:\n return \"CONNECTED\";\n case WebSocket.CLOSING:\n return \"CLOSING\";\n case WebSocket.CLOSED:\n return \"DISCONNECTED\";\n default:\n return \"UNKNOWN\";\n }\n }\n\n public getReconnectAttempts(): number {\n return this.reconnectAttempts;\n }\n\n public getServiceCycles(): number {\n return this.serviceCycles;\n }\n\n public getServiceIndex(): number {\n return this.serviceIndex;\n }\n\n public getAllServices(): string[] {\n return Array.isArray(this.service) ? [...this.service] : [this.service];\n }\n\n public getCurrentService(): string {\n return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;\n }\n\n public getMessageCount(): number {\n return this.messageCount;\n }\n\n public getLastMessageTime(): number {\n return this.lastMessageTime;\n }\n\n public getHealthCheckName(): string {\n return this.healthCheckName;\n }\n}\n","import { Logger } from \"./logger\";\n\nexport interface HealthStatus {\n healthy: boolean;\n timestamp: number;\n checks: Record<string, boolean>;\n metrics: Record<string, number>;\n details?: Record<string, unknown>;\n}\n\nexport interface HealthCheckOptions {\n interval?: number; // milliseconds\n timeout?: number; // milliseconds\n retries?: number;\n}\n\n/**\n * Health monitoring system for bot components.\n * Provides health checks and basic metrics collection.\n */\nexport class HealthMonitor {\n private checks = new Map<string, () => Promise<boolean>>();\n private metrics = new Map<string, number>();\n private lastCheckResults = new Map<string, boolean>();\n private checkInterval: NodeJS.Timeout | null = null;\n private options: Required<HealthCheckOptions>;\n\n constructor(options: HealthCheckOptions = {}) {\n this.options = {\n interval: options.interval || 30000, // 30 seconds\n timeout: options.timeout || 5000, // 5 seconds\n retries: options.retries || 2,\n };\n }\n\n /**\n * Register a health check function.\n * @param name - Unique name for the health check\n * @param checkFn - Function that returns true if healthy\n */\n registerHealthCheck(name: string, checkFn: () => Promise<boolean>) {\n this.checks.set(name, checkFn);\n Logger.debug(`Registered health check: ${name}`);\n }\n\n /**\n * Remove a health check.\n * @param name - Name of the health check to remove\n */\n unregisterHealthCheck(name: string) {\n this.checks.delete(name);\n this.lastCheckResults.delete(name);\n Logger.debug(`Unregistered health check: ${name}`);\n }\n\n /**\n * Set a metric value.\n * @param name - Metric name\n * @param value - Metric value\n */\n setMetric(name: string, value: number) {\n this.metrics.set(name, value);\n }\n\n /**\n * Increment a counter metric.\n * @param name - Metric name\n * @param increment - Value to add (default: 1)\n */\n incrementMetric(name: string, increment = 1) {\n const current = this.metrics.get(name) || 0;\n this.metrics.set(name, current + increment);\n }\n\n /**\n * Get current metric value.\n * @param name - Metric name\n * @returns Current value or 0 if not found\n */\n getMetric(name: string): number {\n return this.metrics.get(name) || 0;\n }\n\n /**\n * Get all current metrics.\n * @returns Object with all metrics\n */\n getAllMetrics(): Record<string, number> {\n return Object.fromEntries(this.metrics);\n }\n\n /**\n * Run a single health check with timeout and retries.\n * @private\n */\n private async runHealthCheck(name: string, checkFn: () => Promise<boolean>): Promise<boolean> {\n for (let attempt = 0; attempt <= this.options.retries; attempt++) {\n try {\n const result = await this.withTimeout(checkFn(), this.options.timeout);\n if (result) {\n return true;\n }\n } catch (error) {\n Logger.debug(\n `Health check \"${name}\" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`,\n { error: error.message }\n );\n }\n }\n return false;\n }\n\n /**\n * Wrap a promise with a timeout.\n * @private\n */\n private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n return Promise.race([\n promise,\n new Promise<T>((_, reject) =>\n setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)\n ),\n ]);\n }\n\n /**\n * Run all health checks and return the current health status.\n */\n async getHealthStatus(): Promise<HealthStatus> {\n const timestamp = Date.now();\n const checkResults: Record<string, boolean> = {};\n const details: Record<string, unknown> = {};\n\n // Run all health checks\n const checkPromises = Array.from(this.checks.entries()).map(async ([name, checkFn]) => {\n const result = await this.runHealthCheck(name, checkFn);\n checkResults[name] = result;\n this.lastCheckResults.set(name, result);\n\n if (!result) {\n details[`${name}_last_failure`] = new Date().toISOString();\n }\n\n return result;\n });\n\n await Promise.allSettled(checkPromises);\n\n // Determine overall health\n const healthy = Object.values(checkResults).every(result => result);\n\n // Get current metrics\n const metrics = this.getAllMetrics();\n\n return {\n healthy,\n timestamp,\n checks: checkResults,\n metrics,\n details,\n };\n }\n\n /**\n * Start periodic health monitoring.\n */\n start() {\n if (this.checkInterval) {\n this.stop();\n }\n\n Logger.info(`Starting health monitor with ${this.options.interval}ms interval`);\n\n this.checkInterval = setInterval(async () => {\n try {\n const status = await this.getHealthStatus();\n\n if (!status.healthy) {\n const failedChecks = Object.entries(status.checks)\n .filter(([, healthy]) => !healthy)\n .map(([name]) => name);\n\n Logger.warn(`Health check failed`, {\n operation: \"health_check\",\n failed_checks: failedChecks,\n metrics: status.metrics,\n });\n } else {\n Logger.debug(\"Health check passed\", {\n operation: \"health_check\",\n metrics: status.metrics,\n });\n }\n } catch (error) {\n Logger.error(\"Error during health check:\", { error: error.message });\n }\n }, this.options.interval);\n }\n\n /**\n * Stop periodic health monitoring.\n */\n stop() {\n if (this.checkInterval) {\n clearInterval(this.checkInterval);\n this.checkInterval = null;\n Logger.info(\"Stopped health monitor\");\n }\n }\n\n /**\n * Get a summary of the last health check results.\n */\n getLastCheckSummary(): Record<string, boolean> {\n return Object.fromEntries(this.lastCheckResults);\n }\n}\n\n// Global health monitor instance\nexport const healthMonitor = new HealthMonitor();\n","import WebSocket from \"ws\";\nimport { WebSocketClient } from \"./websocketClient\";\nimport { Logger } from \"./logger\";\n\n/**\n * Represents a subscription to a Jetstream feed over WebSocket.\n *\n * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats.\n * It invokes a provided callback function whenever a message is received from the Jetstream server.\n */\nexport class JetstreamSubscription extends WebSocketClient {\n /**\n * Creates a new `JetstreamSubscription`.\n *\n * @param service - The URL(-Array) of the Jetstream server(s) to connect to.\n * @param interval - The interval (in milliseconds) for reconnect attempts.\n * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.\n */\n constructor(\n service: string | string[],\n public interval: number,\n private onMessageCallback?: (data: WebSocket.Data) => void\n ) {\n super({ service, reconnectInterval: interval });\n }\n\n /**\n * Called when the WebSocket connection is successfully opened.\n * Logs a message indicating that the connection to the Jetstream server has been established.\n */\n protected onOpen() {\n Logger.info(\"Connected to Jetstream server.\");\n super.onOpen();\n }\n\n /**\n * Called when a WebSocket message is received.\n *\n * If an `onMessageCallback` was provided, it is invoked with the received data.\n *\n * @param data - The data received from the Jetstream server.\n */\n protected onMessage(data: WebSocket.Data) {\n if (this.onMessageCallback) {\n this.onMessageCallback(data);\n }\n }\n\n /**\n * Called when a WebSocket error occurs.\n * Logs the error message indicating that Jetstream encountered an error.\n *\n * @param error - The error that occurred.\n */\n protected onError(error: Error) {\n Logger.error(\"Jetstream encountered an error:\", error);\n super.onError(error);\n }\n\n /**\n * Called when the WebSocket connection is closed.\n * Logs a message indicating that the Jetstream connection has closed.\n */\n protected onClose() {\n Logger.info(\"Jetstream connection closed.\");\n super.onClose();\n }\n}\n","/**\n * Returns the given string if it is defined; otherwise returns `undefined`.\n *\n * @param val - The optional string value to check.\n * @returns The given string if defined, or `undefined` if `val` is falsy.\n */\nexport const maybeStr = (val?: string): string | undefined => {\n if (!val) return undefined;\n return val;\n};\n\n/**\n * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.\n *\n * @param val - The optional string value to parse.\n * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.\n */\nexport const maybeInt = (val?: string): number | undefined => {\n if (!val) return undefined;\n const int = parseInt(val, 10);\n if (isNaN(int)) return undefined;\n return int;\n};\n","import WebSocket from \"ws\";\nimport { Post } from \"../types/post\";\nimport { WebsocketMessage } from \"../types/message\";\n/**\n * Converts a raw WebSocket message into a `FeedEntry` object, if possible.\n *\n * This function checks if the incoming WebSocket data is structured like a feed commit message\n * with the required properties for a created post. If the data matches the expected shape,\n * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`.\n *\n * @param data - The raw WebSocket data.\n * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`.\n */\nexport function websocketToFeedEntry(data: WebSocket.Data): Post | null {\n const message = data as WebsocketMessage;\n if (\n !message.commit ||\n !message.commit.record ||\n !message.commit.record[\"$type\"] ||\n !message.did ||\n !message.commit.cid ||\n !message.commit.rkey ||\n message.commit.operation !== \"create\"\n ) {\n return null;\n }\n const messageUri = `at://${message.did}/${message.commit.record[\"$type\"]}/${message.commit.rkey}`;\n return {\n cid: message.commit.cid,\n uri: messageUri,\n authorDid: message.did,\n text: message.commit.record.text,\n rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,\n rootUri: message.commit.record.reply?.root.uri ?? messageUri,\n };\n}\n"],"mappings":"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","names":["LogLevel","AtpAgent","AtpAgent","AtpAgent","AtpAgent"]}
···11-export * from "./types/bot"
22-export * from "./types/message"
33-export * from "./types/post"
44-export * from "./bots/actionBot"
55-export * from "./bots/cronBot"
66-export * from "./bots/keywordBot"
77-export * from "./utils/jetstreamSubscription"
88-export * from "./utils/logger"
99-export * from "./utils/strings"
1010-export * from "./utils/websocketClient"
1111-export * from "./utils/wsToFeed"
11+export * from "./types/bot";
22+export * from "./types/message";
33+export * from "./types/post";
44+export * from "./bots/actionBot";
55+export * from "./bots/cronBot";
66+export * from "./bots/keywordBot";
77+export * from "./utils/jetstreamSubscription";
88+export * from "./utils/logger";
99+export * from "./utils/strings";
1010+export * from "./utils/websocketClient";
1111+export * from "./utils/wsToFeed";
1212+export * from "./utils/healthCheck";
···11+import { Logger } from "./logger";
22+33+export interface HealthStatus {
44+ healthy: boolean;
55+ timestamp: number;
66+ checks: Record<string, boolean>;
77+ metrics: Record<string, number>;
88+ details?: Record<string, unknown>;
99+}
1010+1111+export interface HealthCheckOptions {
1212+ interval?: number; // milliseconds
1313+ timeout?: number; // milliseconds
1414+ retries?: number;
1515+}
1616+1717+/**
1818+ * Health monitoring system for bot components.
1919+ * Provides health checks and basic metrics collection.
2020+ */
2121+export class HealthMonitor {
2222+ private checks = new Map<string, () => Promise<boolean>>();
2323+ private metrics = new Map<string, number>();
2424+ private lastCheckResults = new Map<string, boolean>();
2525+ private checkInterval: NodeJS.Timeout | null = null;
2626+ private options: Required<HealthCheckOptions>;
2727+2828+ constructor(options: HealthCheckOptions = {}) {
2929+ this.options = {
3030+ interval: options.interval || 30000, // 30 seconds
3131+ timeout: options.timeout || 5000, // 5 seconds
3232+ retries: options.retries || 2,
3333+ };
3434+ }
3535+3636+ /**
3737+ * Register a health check function.
3838+ * @param name - Unique name for the health check
3939+ * @param checkFn - Function that returns true if healthy
4040+ */
4141+ registerHealthCheck(name: string, checkFn: () => Promise<boolean>) {
4242+ this.checks.set(name, checkFn);
4343+ Logger.debug(`Registered health check: ${name}`);
4444+ }
4545+4646+ /**
4747+ * Remove a health check.
4848+ * @param name - Name of the health check to remove
4949+ */
5050+ unregisterHealthCheck(name: string) {
5151+ this.checks.delete(name);
5252+ this.lastCheckResults.delete(name);
5353+ Logger.debug(`Unregistered health check: ${name}`);
5454+ }
5555+5656+ /**
5757+ * Set a metric value.
5858+ * @param name - Metric name
5959+ * @param value - Metric value
6060+ */
6161+ setMetric(name: string, value: number) {
6262+ this.metrics.set(name, value);
6363+ }
6464+6565+ /**
6666+ * Increment a counter metric.
6767+ * @param name - Metric name
6868+ * @param increment - Value to add (default: 1)
6969+ */
7070+ incrementMetric(name: string, increment = 1) {
7171+ const current = this.metrics.get(name) || 0;
7272+ this.metrics.set(name, current + increment);
7373+ }
7474+7575+ /**
7676+ * Get current metric value.
7777+ * @param name - Metric name
7878+ * @returns Current value or 0 if not found
7979+ */
8080+ getMetric(name: string): number {
8181+ return this.metrics.get(name) || 0;
8282+ }
8383+8484+ /**
8585+ * Get all current metrics.
8686+ * @returns Object with all metrics
8787+ */
8888+ getAllMetrics(): Record<string, number> {
8989+ return Object.fromEntries(this.metrics);
9090+ }
9191+9292+ /**
9393+ * Run a single health check with timeout and retries.
9494+ * @private
9595+ */
9696+ private async runHealthCheck(name: string, checkFn: () => Promise<boolean>): Promise<boolean> {
9797+ for (let attempt = 0; attempt <= this.options.retries; attempt++) {
9898+ try {
9999+ const result = await this.withTimeout(checkFn(), this.options.timeout);
100100+ if (result) {
101101+ return true;
102102+ }
103103+ } catch (error) {
104104+ Logger.debug(
105105+ `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`,
106106+ { error: error.message }
107107+ );
108108+ }
109109+ }
110110+ return false;
111111+ }
112112+113113+ /**
114114+ * Wrap a promise with a timeout.
115115+ * @private
116116+ */
117117+ private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
118118+ return Promise.race([
119119+ promise,
120120+ new Promise<T>((_, reject) =>
121121+ setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)
122122+ ),
123123+ ]);
124124+ }
125125+126126+ /**
127127+ * Run all health checks and return the current health status.
128128+ */
129129+ async getHealthStatus(): Promise<HealthStatus> {
130130+ const timestamp = Date.now();
131131+ const checkResults: Record<string, boolean> = {};
132132+ const details: Record<string, unknown> = {};
133133+134134+ // Run all health checks
135135+ const checkPromises = Array.from(this.checks.entries()).map(async ([name, checkFn]) => {
136136+ const result = await this.runHealthCheck(name, checkFn);
137137+ checkResults[name] = result;
138138+ this.lastCheckResults.set(name, result);
139139+140140+ if (!result) {
141141+ details[`${name}_last_failure`] = new Date().toISOString();
142142+ }
143143+144144+ return result;
145145+ });
146146+147147+ await Promise.allSettled(checkPromises);
148148+149149+ // Determine overall health
150150+ const healthy = Object.values(checkResults).every(result => result);
151151+152152+ // Get current metrics
153153+ const metrics = this.getAllMetrics();
154154+155155+ return {
156156+ healthy,
157157+ timestamp,
158158+ checks: checkResults,
159159+ metrics,
160160+ details,
161161+ };
162162+ }
163163+164164+ /**
165165+ * Start periodic health monitoring.
166166+ */
167167+ start() {
168168+ if (this.checkInterval) {
169169+ this.stop();
170170+ }
171171+172172+ Logger.info(`Starting health monitor with ${this.options.interval}ms interval`);
173173+174174+ this.checkInterval = setInterval(async () => {
175175+ try {
176176+ const status = await this.getHealthStatus();
177177+178178+ if (!status.healthy) {
179179+ const failedChecks = Object.entries(status.checks)
180180+ .filter(([, healthy]) => !healthy)
181181+ .map(([name]) => name);
182182+183183+ Logger.warn(`Health check failed`, {
184184+ operation: "health_check",
185185+ failed_checks: failedChecks,
186186+ metrics: status.metrics,
187187+ });
188188+ } else {
189189+ Logger.debug("Health check passed", {
190190+ operation: "health_check",
191191+ metrics: status.metrics,
192192+ });
193193+ }
194194+ } catch (error) {
195195+ Logger.error("Error during health check:", { error: error.message });
196196+ }
197197+ }, this.options.interval);
198198+ }
199199+200200+ /**
201201+ * Stop periodic health monitoring.
202202+ */
203203+ stop() {
204204+ if (this.checkInterval) {
205205+ clearInterval(this.checkInterval);
206206+ this.checkInterval = null;
207207+ Logger.info("Stopped health monitor");
208208+ }
209209+ }
210210+211211+ /**
212212+ * Get a summary of the last health check results.
213213+ */
214214+ getLastCheckSummary(): Record<string, boolean> {
215215+ return Object.fromEntries(this.lastCheckResults);
216216+ }
217217+}
218218+219219+// Global health monitor instance
220220+export const healthMonitor = new HealthMonitor();
+55-52
src/utils/jetstreamSubscription.ts
···11-import WebSocket from 'ws';
22-import { WebSocketClient } from './websocketClient';
33-import { Logger } from './logger';
11+import WebSocket from "ws";
22+import { WebSocketClient } from "./websocketClient";
33+import { Logger } from "./logger";
4455/**
66 * Represents a subscription to a Jetstream feed over WebSocket.
77- *
77+ *
88 * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats.
99 * It invokes a provided callback function whenever a message is received from the Jetstream server.
1010 */
1111export class JetstreamSubscription extends WebSocketClient {
1212- /**
1313- * Creates a new `JetstreamSubscription`.
1414- *
1515- * @param service - The URL of the Jetstream server to connect to.
1616- * @param interval - The interval (in milliseconds) for reconnect attempts.
1717- * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
1818- */
1919- constructor(
2020- public service: string,
2121- public interval: number,
2222- private onMessageCallback?: (data: WebSocket.Data) => void
2323- ) {
2424- super({url: service, reconnectInterval: interval});
2525- }
1212+ /**
1313+ * Creates a new `JetstreamSubscription`.
1414+ *
1515+ * @param service - The URL(-Array) of the Jetstream server(s) to connect to.
1616+ * @param interval - The interval (in milliseconds) for reconnect attempts.
1717+ * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server.
1818+ */
1919+ constructor(
2020+ service: string | string[],
2121+ public interval: number,
2222+ private onMessageCallback?: (data: WebSocket.Data) => void
2323+ ) {
2424+ super({ service, reconnectInterval: interval });
2525+ }
26262727- /**
2828- * Called when the WebSocket connection is successfully opened.
2929- * Logs a message indicating that the connection to the Jetstream server has been established.
3030- */
3131- protected onOpen() {
3232- Logger.info('Connected to Jetstream server.');
3333- }
2727+ /**
2828+ * Called when the WebSocket connection is successfully opened.
2929+ * Logs a message indicating that the connection to the Jetstream server has been established.
3030+ */
3131+ protected onOpen() {
3232+ Logger.info("Connected to Jetstream server.");
3333+ super.onOpen();
3434+ }
34353535- /**
3636- * Called when a WebSocket message is received.
3737- *
3838- * If an `onMessageCallback` was provided, it is invoked with the received data.
3939- *
4040- * @param data - The data received from the Jetstream server.
4141- */
4242- protected onMessage(data: WebSocket.Data) {
4343- if (this.onMessageCallback) {
4444- this.onMessageCallback(data);
4545- }
3636+ /**
3737+ * Called when a WebSocket message is received.
3838+ *
3939+ * If an `onMessageCallback` was provided, it is invoked with the received data.
4040+ *
4141+ * @param data - The data received from the Jetstream server.
4242+ */
4343+ protected onMessage(data: WebSocket.Data) {
4444+ if (this.onMessageCallback) {
4545+ this.onMessageCallback(data);
4646 }
4747+ }
47484848- /**
4949- * Called when a WebSocket error occurs.
5050- * Logs the error message indicating that Jetstream encountered an error.
5151- *
5252- * @param error - The error that occurred.
5353- */
5454- protected onError(error: Error) {
5555- Logger.error('Jetstream encountered an error:', error);
5656- }
4949+ /**
5050+ * Called when a WebSocket error occurs.
5151+ * Logs the error message indicating that Jetstream encountered an error.
5252+ *
5353+ * @param error - The error that occurred.
5454+ */
5555+ protected onError(error: Error) {
5656+ Logger.error("Jetstream encountered an error:", error);
5757+ super.onError(error);
5858+ }
57595858- /**
5959- * Called when the WebSocket connection is closed.
6060- * Logs a message indicating that the Jetstream connection has closed.
6161- */
6262- protected onClose() {
6363- Logger.info('Jetstream connection closed.');
6464- }
6060+ /**
6161+ * Called when the WebSocket connection is closed.
6262+ * Logs a message indicating that the Jetstream connection has closed.
6363+ */
6464+ protected onClose() {
6565+ Logger.info("Jetstream connection closed.");
6666+ super.onClose();
6767+ }
6568}
+202-34
src/utils/logger.ts
···11+export enum LogLevel {
22+ DEBUG = 0,
33+ INFO = 1,
44+ WARN = 2,
55+ ERROR = 3,
66+}
77+88+export interface LogContext {
99+ correlationId?: string;
1010+ botId?: string;
1111+ operation?: string;
1212+ duration?: number;
1313+ [key: string]: unknown;
1414+}
1515+116/**
22- * A simple logging utility class providing static methods for various log levels.
1717+ * A performance-optimized logging utility class providing static methods for various log levels.
318 * Each log message is prefixed with a timestamp and log level.
1919+ * Supports conditional logging based on log levels and configurable timezone.
420 */
521export class Logger {
66- /**
77- * Logs an informational message to the console.
88- *
99- * @param message - The message to be logged.
1010- * @param context - Optional additional context (object or string) to log alongside the message.
1111- */
1212- static info(message: string, context?: object | string) {
1313- console.info(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [INFO]: ${message}`, context || '');
2222+ private static logLevel: LogLevel = LogLevel.INFO;
2323+ private static timezone: string = "Europe/Vienna";
2424+ private static correlationId: string | null = null;
2525+2626+ /**
2727+ * Generate a new correlation ID for tracking related operations.
2828+ */
2929+ static generateCorrelationId(): string {
3030+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
3131+ }
3232+3333+ /**
3434+ * Set the correlation ID for subsequent log entries.
3535+ * @param id - The correlation ID to use, or null to generate a new one
3636+ */
3737+ static setCorrelationId(id?: string | null) {
3838+ this.correlationId = id || this.generateCorrelationId();
3939+ }
4040+4141+ /**
4242+ * Get the current correlation ID.
4343+ */
4444+ static getCorrelationId(): string | null {
4545+ return this.correlationId;
4646+ }
4747+4848+ /**
4949+ * Clear the current correlation ID.
5050+ */
5151+ static clearCorrelationId() {
5252+ this.correlationId = null;
5353+ }
5454+5555+ /**
5656+ * Set the minimum log level. Messages below this level will not be logged.
5757+ * @param level - The minimum log level
5858+ */
5959+ static setLogLevel(level: LogLevel) {
6060+ this.logLevel = level;
6161+ }
6262+6363+ /**
6464+ * Set the timezone for log timestamps.
6565+ * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC")
6666+ */
6767+ static setTimezone(timezone: string) {
6868+ this.timezone = timezone;
6969+ }
7070+7171+ /**
7272+ * Get the current log level.
7373+ */
7474+ static getLogLevel(): LogLevel {
7575+ return this.logLevel;
7676+ }
7777+7878+ /**
7979+ * Generate a formatted timestamp string.
8080+ * @private
8181+ */
8282+ private static getTimestamp(): string {
8383+ return new Date().toLocaleString("de-DE", { timeZone: this.timezone });
8484+ }
8585+8686+ /**
8787+ * Internal logging method that checks log level before processing.
8888+ * @private
8989+ */
9090+ private static log(
9191+ level: LogLevel,
9292+ levelName: string,
9393+ message: string,
9494+ context?: LogContext | object | string,
9595+ logFn = console.log
9696+ ) {
9797+ if (level < this.logLevel) {
9898+ return; // Skip logging if below threshold
1499 }
151001616- /**
1717- * Logs a warning message to the console.
1818- *
1919- * @param message - The message to be logged.
2020- * @param context - Optional additional context (object or string) to log alongside the message.
2121- */
2222- static warn(message: string, context?: object | string) {
2323- console.warn(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [WARNING]: ${message}`, context || '');
101101+ const timestamp = this.getTimestamp();
102102+ let formattedMessage = `${timestamp} [${levelName}]`;
103103+104104+ // Add correlation ID if available
105105+ if (this.correlationId) {
106106+ formattedMessage += ` [${this.correlationId}]`;
24107 }
251082626- /**
2727- * Logs an error message to the console.
2828- *
2929- * @param message - The message to be logged.
3030- * @param context - Optional additional context (object or string) to log alongside the message.
3131- */
3232- static error(message: string, context?: object | string) {
3333- console.error(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [ERROR]: ${message}`, context || '');
109109+ // Add context correlation ID if provided and different from global one
110110+ if (
111111+ context &&
112112+ typeof context === "object" &&
113113+ "correlationId" in context &&
114114+ context.correlationId &&
115115+ context.correlationId !== this.correlationId
116116+ ) {
117117+ formattedMessage += ` [${context.correlationId}]`;
34118 }
351193636- /**
3737- * Logs a debug message to the console.
3838- *
3939- * @param message - The message to be logged.
4040- * @param context - Optional additional context (object or string) to log alongside the message.
4141- */
4242- static debug(message: string, context?: object | string) {
4343- console.debug(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [DEBUG]: ${message}`, context || '');
120120+ formattedMessage += `: ${message}`;
121121+122122+ if (context) {
123123+ // Create structured log entry for objects
124124+ if (typeof context === "object") {
125125+ const logEntry = {
126126+ timestamp: new Date().toISOString(),
127127+ level: levelName,
128128+ message,
129129+ correlationId: this.correlationId,
130130+ ...context,
131131+ };
132132+ logFn(formattedMessage, logEntry);
133133+ } else {
134134+ logFn(formattedMessage, context);
135135+ }
136136+ } else {
137137+ logFn(formattedMessage);
44138 }
4545-}139139+ }
140140+ /**
141141+ * Logs an informational message to the console.
142142+ *
143143+ * @param message - The message to be logged.
144144+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
145145+ */
146146+ static info(message: string, context?: LogContext | object | string) {
147147+ this.log(LogLevel.INFO, "INFO", message, context, console.info);
148148+ }
149149+150150+ /**
151151+ * Logs a warning message to the console.
152152+ *
153153+ * @param message - The message to be logged.
154154+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
155155+ */
156156+ static warn(message: string, context?: LogContext | object | string) {
157157+ this.log(LogLevel.WARN, "WARNING", message, context, console.warn);
158158+ }
159159+160160+ /**
161161+ * Logs an error message to the console.
162162+ *
163163+ * @param message - The message to be logged.
164164+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
165165+ */
166166+ static error(message: string, context?: LogContext | object | string) {
167167+ this.log(LogLevel.ERROR, "ERROR", message, context, console.error);
168168+ }
169169+170170+ /**
171171+ * Logs a debug message to the console.
172172+ *
173173+ * @param message - The message to be logged.
174174+ * @param context - Optional additional context (LogContext, object or string) to log alongside the message.
175175+ */
176176+ static debug(message: string, context?: LogContext | object | string) {
177177+ this.log(LogLevel.DEBUG, "DEBUG", message, context, console.debug);
178178+ }
179179+180180+ /**
181181+ * Log operation start with timing.
182182+ * @param operation - The operation name
183183+ * @param context - Additional context
184184+ */
185185+ static startOperation(operation: string, context?: LogContext): string {
186186+ const correlationId = context?.correlationId || this.generateCorrelationId();
187187+ this.setCorrelationId(correlationId);
188188+189189+ this.info(`Starting operation: ${operation}`, {
190190+ operation,
191191+ correlationId,
192192+ ...context,
193193+ });
194194+195195+ return correlationId;
196196+ }
197197+198198+ /**
199199+ * Log operation completion with timing.
200200+ * @param operation - The operation name
201201+ * @param startTime - The start time from Date.now()
202202+ * @param context - Additional context
203203+ */
204204+ static endOperation(operation: string, startTime: number, context?: LogContext) {
205205+ const duration = Date.now() - startTime;
206206+207207+ this.info(`Completed operation: ${operation}`, {
208208+ operation,
209209+ duration: `${duration}ms`,
210210+ ...context,
211211+ });
212212+ }
213213+}
+8-8
src/utils/strings.ts
···11/**
22 * Returns the given string if it is defined; otherwise returns `undefined`.
33- *
33+ *
44 * @param val - The optional string value to check.
55 * @returns The given string if defined, or `undefined` if `val` is falsy.
66 */
77export const maybeStr = (val?: string): string | undefined => {
88 if (!val) return undefined;
99 return val;
1010-}
1010+};
11111212/**
1313-* Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
1414-*
1515-* @param val - The optional string value to parse.
1616-* @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
1717-*/
1313+ * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`.
1414+ *
1515+ * @param val - The optional string value to parse.
1616+ * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer.
1717+ */
1818export const maybeInt = (val?: string): number | undefined => {
1919 if (!val) return undefined;
2020 const int = parseInt(val, 10);
2121 if (isNaN(int)) return undefined;
2222 return int;
2323-}2323+};
+346-129
src/utils/websocketClient.ts
···11-import WebSocket from 'ws';
22-import { Logger } from './logger';
11+import WebSocket from "ws";
22+import { Logger } from "./logger";
33+import { healthMonitor } from "./healthCheck";
3445interface WebSocketClientOptions {
55- /** The URL of the WebSocket server to connect to. */
66- url: string;
77- /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */
88- reconnectInterval?: number;
99- /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */
1010- pingInterval?: number;
66+ /** The URL of the WebSocket server to connect to. */
77+ service: string | string[];
88+ /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */
99+ reconnectInterval?: number;
1010+ /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */
1111+ pingInterval?: number;
1212+ /** Maximum number of consecutive reconnection attempts per service. Default is 3. */
1313+ maxReconnectAttempts?: number;
1414+ /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */
1515+ maxReconnectDelay?: number;
1616+ /** Exponential backoff factor for reconnection delays. Default is 1.5. */
1717+ backoffFactor?: number;
1818+ /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */
1919+ maxServiceCycles?: number;
1120}
12211322/**
1423 * A WebSocket client that automatically attempts to reconnect upon disconnection
1524 * and periodically sends ping messages (heartbeats) to ensure the connection remains alive.
1616- *
2525+ *
1726 * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods
1827 * to implement custom handling of WebSocket events.
1928 */
2029export class WebSocketClient {
2121- private url: string;
2222- private reconnectInterval: number;
2323- private pingInterval: number;
2424- private ws: WebSocket | null = null;
2525- private pingTimeout: NodeJS.Timeout | null = null;
3030+ private service: string | string[];
3131+ private reconnectInterval: number;
3232+ private pingInterval: number;
3333+ private ws: WebSocket | null = null;
3434+ private pingTimeout: NodeJS.Timeout | null = null;
3535+ private serviceIndex = 0;
3636+ private reconnectAttempts = 0;
3737+ private serviceCycles = 0;
3838+ private maxReconnectAttempts: number;
3939+ private maxServiceCycles: number;
4040+ private maxReconnectDelay: number;
4141+ private backoffFactor: number;
4242+ private reconnectTimeout: NodeJS.Timeout | null = null;
4343+ private isConnecting = false;
4444+ private shouldReconnect = true;
4545+ private messageCount = 0;
4646+ private lastMessageTime = 0;
4747+ private healthCheckName: string;
4848+4949+ /**
5050+ * Creates a new instance of `WebSocketClient`.
5151+ *
5252+ * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.
5353+ */
5454+ constructor(options: WebSocketClientOptions) {
5555+ this.service = options.service;
5656+ this.reconnectInterval = options.reconnectInterval || 5000;
5757+ this.pingInterval = options.pingInterval || 10000;
5858+ this.maxReconnectAttempts = options.maxReconnectAttempts || 3;
5959+ this.maxServiceCycles = options.maxServiceCycles || 2;
6060+ this.maxReconnectDelay = options.maxReconnectDelay || 30000;
6161+ this.backoffFactor = options.backoffFactor || 1.5;
6262+6363+ // Generate unique health check name
6464+ this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
6565+6666+ // Register health check
6767+ healthMonitor.registerHealthCheck(this.healthCheckName, async () => {
6868+ return this.getConnectionState() === "CONNECTED";
6969+ });
26702727- /**
2828- * Creates a new instance of `WebSocketClient`.
2929- *
3030- * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval.
3131- */
3232- constructor(options: WebSocketClientOptions) {
3333- this.url = options.url;
3434- this.reconnectInterval = options.reconnectInterval || 5000;
3535- this.pingInterval = options.pingInterval || 10000;
3636- this.run();
7171+ // Initialize metrics
7272+ healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0);
7373+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0);
7474+7575+ this.run();
7676+ }
7777+7878+ /**
7979+ * Initiates a WebSocket connection to the specified URL.
8080+ *
8181+ * This method sets up event listeners for `open`, `message`, `error`, and `close` events.
8282+ * When the connection opens, it starts the heartbeat mechanism.
8383+ * On close, it attempts to reconnect after a specified interval.
8484+ */
8585+ private run() {
8686+ if (this.isConnecting) {
8787+ return;
3788 }
38893939- /**
4040- * Initiates a WebSocket connection to the specified URL.
4141- *
4242- * This method sets up event listeners for `open`, `message`, `error`, and `close` events.
4343- * When the connection opens, it starts the heartbeat mechanism.
4444- * On close, it attempts to reconnect after a specified interval.
4545- */
4646- private run() {
4747- this.ws = new WebSocket(this.url);
9090+ this.isConnecting = true;
9191+ const currentService = Array.isArray(this.service)
9292+ ? this.service[this.serviceIndex]
9393+ : this.service;
48944949- this.ws.on('open', () => {
5050- Logger.info('WebSocket connected');
5151- this.startHeartbeat();
5252- this.onOpen();
5353- });
9595+ Logger.info(`Attempting to connect to WebSocket: ${currentService}`);
9696+ this.ws = new WebSocket(currentService);
54975555- this.ws.on('message', (data: WebSocket.Data) => {
5656- this.onMessage(data);
9898+ this.ws.on("open", () => {
9999+ Logger.info("WebSocket connected successfully", {
100100+ service: this.getCurrentService(),
101101+ serviceIndex: this.serviceIndex,
102102+ });
103103+ this.isConnecting = false;
104104+ this.reconnectAttempts = 0; // Reset on successful connection
105105+ this.serviceCycles = 0; // Reset cycles on successful connection
106106+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
107107+ this.startHeartbeat();
108108+ this.onOpen();
109109+ });
110110+111111+ this.ws.on("message", (data: WebSocket.Data) => {
112112+ this.messageCount++;
113113+ this.lastMessageTime = Date.now();
114114+ healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`);
115115+ this.onMessage(data);
116116+ });
117117+118118+ this.ws.on("error", error => {
119119+ Logger.error("WebSocket error:", error);
120120+ this.isConnecting = false;
121121+ this.onError(error);
122122+ });
123123+124124+ this.ws.on("close", (code, reason) => {
125125+ Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`);
126126+ this.isConnecting = false;
127127+ this.stopHeartbeat();
128128+ this.onClose();
129129+130130+ if (this.shouldReconnect) {
131131+ this.scheduleReconnect();
132132+ }
133133+ });
134134+ }
135135+136136+ /**
137137+ * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
138138+ * It clears all event listeners on the old WebSocket and initiates a new connection.
139139+ */
140140+ private scheduleReconnect() {
141141+ this.reconnectAttempts++;
142142+ healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts);
143143+144144+ // Check if we should try the next service
145145+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
146146+ if (this.shouldTryNextService()) {
147147+ this.moveToNextService();
148148+ return; // Try next service immediately
149149+ } else {
150150+ Logger.error("All services exhausted after maximum cycles", {
151151+ totalServices: Array.isArray(this.service) ? this.service.length : 1,
152152+ maxServiceCycles: this.maxServiceCycles,
153153+ serviceCycles: this.serviceCycles,
57154 });
155155+ return; // Give up entirely
156156+ }
157157+ }
581585959- this.ws.on('error', (error) => {
6060- Logger.error('WebSocket error:', error);
6161- this.onError(error);
6262- });
159159+ const delay = Math.min(
160160+ this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1),
161161+ this.maxReconnectDelay
162162+ );
631636464- this.ws.on('close', () => {
6565- Logger.info('WebSocket disconnected');
6666- this.stopHeartbeat();
6767- this.onClose();
6868- this.reconnect();
6969- });
164164+ Logger.info(
165165+ `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`,
166166+ {
167167+ service: this.getCurrentService(),
168168+ serviceIndex: this.serviceIndex,
169169+ delay: `${delay}ms`,
170170+ }
171171+ );
172172+173173+ if (this.reconnectTimeout) {
174174+ clearTimeout(this.reconnectTimeout);
175175+ }
176176+177177+ this.reconnectTimeout = setTimeout(() => {
178178+ this.cleanup();
179179+ this.run();
180180+ }, delay);
181181+ }
182182+183183+ /**
184184+ * Check if we should try the next service in the array.
185185+ */
186186+ private shouldTryNextService(): boolean {
187187+ if (!Array.isArray(this.service)) {
188188+ return false; // Single service, can't switch
70189 }
711907272- /**
7373- * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`.
7474- * It clears all event listeners on the old WebSocket and initiates a new connection.
7575- */
7676- private reconnect() {
7777- if (this.ws) {
7878- this.ws.removeAllListeners();
7979- this.ws = null;
8080- }
191191+ return this.serviceCycles < this.maxServiceCycles;
192192+ }
811938282- setTimeout(() => this.run(), this.reconnectInterval);
194194+ /**
195195+ * Move to the next service in the array and reset reconnection attempts.
196196+ */
197197+ private moveToNextService() {
198198+ if (!Array.isArray(this.service)) {
199199+ return;
83200 }
842018585- /**
8686- * Starts sending periodic ping messages to the server.
8787- *
8888- * This function uses `setInterval` to send a ping at the configured `pingInterval`.
8989- * If the WebSocket is not open, pings are not sent.
9090- */
9191- private startHeartbeat() {
9292- this.pingTimeout = setInterval(() => {
9393- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
9494- this.ws.ping();
9595- }
9696- }, this.pingInterval);
202202+ const previousIndex = this.serviceIndex;
203203+ this.serviceIndex = (this.serviceIndex + 1) % this.service.length;
204204+205205+ // If we've gone through all services once, increment the cycle counter
206206+ if (this.serviceIndex === 0) {
207207+ this.serviceCycles++;
97208 }
982099999- /**
100100- * Stops sending heartbeat pings by clearing the ping interval.
101101- */
102102- private stopHeartbeat() {
103103- if (this.pingTimeout) {
104104- clearInterval(this.pingTimeout);
105105- this.pingTimeout = null;
106106- }
210210+ this.reconnectAttempts = 0; // Reset attempts for the new service
211211+212212+ Logger.info("Switching to next service", {
213213+ previousService: this.service[previousIndex],
214214+ previousIndex,
215215+ newService: this.getCurrentService(),
216216+ newIndex: this.serviceIndex,
217217+ serviceCycle: this.serviceCycles,
218218+ });
219219+220220+ // Try the new service immediately
221221+ this.cleanup();
222222+ this.run();
223223+ }
224224+225225+ private cleanup() {
226226+ if (this.ws) {
227227+ this.ws.removeAllListeners();
228228+ if (this.ws.readyState === WebSocket.OPEN) {
229229+ this.ws.close();
230230+ }
231231+ this.ws = null;
107232 }
108233109109- /**
110110- * Called when the WebSocket connection is successfully opened.
111111- *
112112- * Override this method in a subclass to implement custom logic on connection.
113113- */
114114- protected onOpen() {
115115- // Custom logic for connection open
234234+ if (this.reconnectTimeout) {
235235+ clearTimeout(this.reconnectTimeout);
236236+ this.reconnectTimeout = null;
116237 }
238238+ }
117239118118- /**
119119- * Called when a WebSocket message is received.
120120- *
121121- * @param data - The data received from the WebSocket server.
122122- *
123123- * Override this method in a subclass to implement custom message handling.
124124- */
125125- protected onMessage(data: WebSocket.Data) {
126126- // Custom logic for handling received messages
240240+ /**
241241+ * Starts sending periodic ping messages to the server.
242242+ *
243243+ * This function uses `setInterval` to send a ping at the configured `pingInterval`.
244244+ * If the WebSocket is not open, pings are not sent.
245245+ */
246246+ private startHeartbeat() {
247247+ this.pingTimeout = setInterval(() => {
248248+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
249249+ this.ws.ping();
250250+ }
251251+ }, this.pingInterval);
252252+ }
253253+254254+ /**
255255+ * Stops sending heartbeat pings by clearing the ping interval.
256256+ */
257257+ private stopHeartbeat() {
258258+ if (this.pingTimeout) {
259259+ clearInterval(this.pingTimeout);
260260+ this.pingTimeout = null;
127261 }
262262+ }
128263129129- /**
130130- * Called when a WebSocket error occurs.
131131- *
132132- * @param error - The error that occurred.
133133- *
134134- * Override this method in a subclass to implement custom error handling.
135135- */
136136- protected onError(error: Error) {
137137- // Custom logic for handling errors
264264+ /**
265265+ * Called when the WebSocket connection is successfully opened.
266266+ *
267267+ * Override this method in a subclass to implement custom logic on connection.
268268+ */
269269+ protected onOpen() {
270270+ // Custom logic for connection open
271271+ }
272272+273273+ /**
274274+ * Called when a WebSocket message is received.
275275+ *
276276+ * @param data - The data received from the WebSocket server.
277277+ *
278278+ * Override this method in a subclass to implement custom message handling.
279279+ */
280280+ protected onMessage(_data: WebSocket.Data) {
281281+ // Custom logic for handling received messages
282282+ }
283283+284284+ /**
285285+ * Called when a WebSocket error occurs.
286286+ *
287287+ * @param error - The error that occurred.
288288+ *
289289+ * Override this method in a subclass to implement custom error handling.
290290+ * Note: Service switching is now handled in the reconnection logic, not here.
291291+ */
292292+ protected onError(_error: Error) {
293293+ // Custom logic for handling errors - override in subclasses
294294+ // Service switching is handled automatically in scheduleReconnect()
295295+ }
296296+297297+ /**
298298+ * Called when the WebSocket connection is closed.
299299+ *
300300+ * Override this method in a subclass to implement custom logic on disconnection.
301301+ */
302302+ protected onClose() {
303303+ // Custom logic for handling connection close
304304+ }
305305+306306+ /**
307307+ * Sends data to the connected WebSocket server, if the connection is open.
308308+ *
309309+ * @param data - The data to send.
310310+ */
311311+ public send(data: string | Buffer | ArrayBuffer | Buffer[]) {
312312+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
313313+ this.ws.send(data);
138314 }
315315+ }
139316140140- /**
141141- * Called when the WebSocket connection is closed.
142142- *
143143- * Override this method in a subclass to implement custom logic on disconnection.
144144- */
145145- protected onClose() {
146146- // Custom logic for handling connection close
317317+ /**
318318+ * Closes the WebSocket connection gracefully.
319319+ */
320320+ public close() {
321321+ this.shouldReconnect = false;
322322+ this.stopHeartbeat();
323323+324324+ if (this.reconnectTimeout) {
325325+ clearTimeout(this.reconnectTimeout);
326326+ this.reconnectTimeout = null;
147327 }
148328149149- /**
150150- * Sends data to the connected WebSocket server, if the connection is open.
151151- *
152152- * @param data - The data to send.
153153- */
154154- public send(data: any) {
155155- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
156156- this.ws.send(data);
157157- }
329329+ if (this.ws) {
330330+ this.ws.close();
158331 }
159332160160- /**
161161- * Closes the WebSocket connection gracefully.
162162- */
163163- public close() {
164164- if (this.ws) {
165165- this.ws.close();
166166- }
333333+ // Unregister health check when closing
334334+ healthMonitor.unregisterHealthCheck(this.healthCheckName);
335335+ }
336336+337337+ public getConnectionState(): string {
338338+ if (!this.ws) return "DISCONNECTED";
339339+340340+ switch (this.ws.readyState) {
341341+ case WebSocket.CONNECTING:
342342+ return "CONNECTING";
343343+ case WebSocket.OPEN:
344344+ return "CONNECTED";
345345+ case WebSocket.CLOSING:
346346+ return "CLOSING";
347347+ case WebSocket.CLOSED:
348348+ return "DISCONNECTED";
349349+ default:
350350+ return "UNKNOWN";
167351 }
168168-}352352+ }
353353+354354+ public getReconnectAttempts(): number {
355355+ return this.reconnectAttempts;
356356+ }
357357+358358+ public getServiceCycles(): number {
359359+ return this.serviceCycles;
360360+ }
361361+362362+ public getServiceIndex(): number {
363363+ return this.serviceIndex;
364364+ }
365365+366366+ public getAllServices(): string[] {
367367+ return Array.isArray(this.service) ? [...this.service] : [this.service];
368368+ }
369369+370370+ public getCurrentService(): string {
371371+ return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service;
372372+ }
373373+374374+ public getMessageCount(): number {
375375+ return this.messageCount;
376376+ }
377377+378378+ public getLastMessageTime(): number {
379379+ return this.lastMessageTime;
380380+ }
381381+382382+ public getHealthCheckName(): string {
383383+ return this.healthCheckName;
384384+ }
385385+}
+26-20
src/utils/wsToFeed.ts
···11-import WebSocket from 'ws';
11+import WebSocket from "ws";
22import { Post } from "../types/post";
33-import { WebsocketMessage } from '../types/message';
44-;
55-33+import { WebsocketMessage } from "../types/message";
64/**
75 * Converts a raw WebSocket message into a `FeedEntry` object, if possible.
88- *
66+ *
97 * This function checks if the incoming WebSocket data is structured like a feed commit message
108 * with the required properties for a created post. If the data matches the expected shape,
119 * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`.
1212- *
1010+ *
1311 * @param data - The raw WebSocket data.
1412 * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`.
1513 */
1614export function websocketToFeedEntry(data: WebSocket.Data): Post | null {
1717- const message = data as WebsocketMessage;
1818- if(!message.commit || !message.commit.record || !message.commit.record['$type'] || !message.did || !message.commit.cid || !message.commit.rkey || message.commit.operation !== "create") {
1919- return null;
2020- }
2121- const messageUri = `at://${message.did}/${message.commit.record['$type']}/${message.commit.rkey}`;
2222- return {
2323- cid: message.commit.cid,
2424- uri: messageUri,
2525- authorDid: message.did,
2626- text: message.commit.record.text,
2727- rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,
2828- rootUri: message.commit.record.reply?.root.uri ?? messageUri,
2929- };
3030-}1515+ const message = data as WebsocketMessage;
1616+ if (
1717+ !message.commit ||
1818+ !message.commit.record ||
1919+ !message.commit.record["$type"] ||
2020+ !message.did ||
2121+ !message.commit.cid ||
2222+ !message.commit.rkey ||
2323+ message.commit.operation !== "create"
2424+ ) {
2525+ return null;
2626+ }
2727+ const messageUri = `at://${message.did}/${message.commit.record["$type"]}/${message.commit.rkey}`;
2828+ return {
2929+ cid: message.commit.cid,
3030+ uri: messageUri,
3131+ authorDid: message.did,
3232+ text: message.commit.record.text,
3333+ rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid,
3434+ rootUri: message.commit.record.reply?.root.uri ?? messageUri,
3535+ };
3636+}