Bots to use with the firehose/jetstream

major overhaul, more resilient code, better structure, logging, healthCheck etc.

bskybot fc281705 fd6d287e

+4
.husky/pre-commit
··· 1 + #!/usr/bin/env sh 2 + . "$(dirname -- "$0")/_/husky.sh" 3 + 4 + npx lint-staged
+6
.prettierignore
··· 1 + dist/ 2 + node_modules/ 3 + *.md 4 + *.json 5 + *.yml 6 + *.yaml
+11
.prettierrc
··· 1 + { 2 + "semi": true, 3 + "trailingComma": "es5", 4 + "singleQuote": false, 5 + "printWidth": 100, 6 + "tabWidth": 2, 7 + "useTabs": false, 8 + "bracketSpacing": true, 9 + "arrowParens": "avoid", 10 + "endOfLine": "lf" 11 + }
+77 -5
README.md
··· 14 14 import { 15 15 ActionBot, 16 16 CronBot, 17 - KexwordBot 17 + KeywordBot 18 18 } from "bskybot"; 19 19 20 20 const actionBot: ActionBot = { ··· 23 23 username: "[handle]", // optional for logging needed 24 24 service: "https://bsky.social", // or another 25 25 action: async (agent: AtpAgent) => { 26 - // implement any logic you want here to be repeated at the scheduledExpression 26 + // implement any logic you want here 27 27 const text = "implement logic to return a string"; 28 - console.info(new Date, `Post cronbot ${bot.identifier}: ${text}`) 28 + console.info(new Date(), `Post actionbot ${actionBot.identifier}: ${text}`); 29 29 agent.post({text}); 30 30 } 31 31 } ··· 49 49 action: async (agent: AtpAgent) => { 50 50 // implement any logic you want here to be executed in your project 51 51 const text = "implement logic to return a string"; 52 - console.info(new Date, `Post cronbot ${bot.identifier}: ${text}`) 52 + console.info(new Date(), `Post cronbot ${cronBot.identifier}: ${text}`); 53 53 agent.post({text}); 54 54 } 55 55 } ··· 79 79 } 80 80 81 81 const keywordBotAgent = useKeywordBotAgent(keywordBot); 82 - ``` 82 + ``` 83 + 84 + ## Migration Guide 85 + 86 + ### Migrating from v1.x to v2.0.0 87 + 88 + Version 2.0.0 introduces breaking changes that require code updates when upgrading from v1.x. 89 + 90 + #### Breaking Changes 91 + 92 + 1. **WebSocket Configuration Change** 93 + 94 + The `WebSocketClient` constructor parameter has changed from `url` to `service` and now supports multiple services for failover: 95 + 96 + ```typescript 97 + // Before (v1.x) 98 + import { WebSocketClient } from "bskybot"; 99 + 100 + const client = new WebSocketClient({ 101 + url: 'wss://example.com' 102 + }); 103 + 104 + // After (v2.0.0) 105 + const client = new WebSocketClient({ 106 + service: 'wss://example.com' // Single service 107 + }); 108 + 109 + // Or with multiple services for automatic failover: 110 + const client = new WebSocketClient({ 111 + service: ['wss://primary.com', 'wss://backup.com', 'wss://fallback.com'] 112 + }); 113 + ``` 114 + 115 + 2. **WebSocket Send Method Typing** 116 + 117 + The `send()` method now has stricter typing for better type safety: 118 + 119 + ```typescript 120 + // Before (v1.x) - accepted any data type 121 + client.send(anyData); 122 + 123 + // After (v2.0.0) - only accepts specific types 124 + client.send("string data"); // ✓ Valid 125 + client.send(buffer); // ✓ Valid (Buffer) 126 + client.send(arrayBuffer); // ✓ Valid (ArrayBuffer) 127 + client.send(bufferArray); // ✓ Valid (Buffer[]) 128 + client.send({ custom: "object" }); // ✗ Invalid - will cause TypeScript error 129 + ``` 130 + 131 + #### New Features in v2.0.0 132 + 133 + - **Multi-Service WebSocket Failover**: Automatically switches between services when connections fail 134 + - **Enhanced Logging**: Structured logging with correlation IDs for better debugging 135 + - **Health Monitoring**: Built-in health checks and metrics collection 136 + - **Performance Optimizations**: Improved error handling and retry strategies 137 + - **Code Quality**: Full ESLint and Prettier integration with pre-commit hooks 138 + 139 + #### Configuration Options 140 + 141 + The WebSocketClient now supports additional configuration options: 142 + 143 + ```typescript 144 + const client = new WebSocketClient({ 145 + service: ['wss://primary.com', 'wss://backup.com'], 146 + maxReconnectAttempts: 3, // Attempts per service (default: 3) 147 + maxServiceCycles: 2, // Complete cycles through all services (default: 2) 148 + reconnectInterval: 5000, // Initial delay between attempts (default: 5000ms) 149 + backoffFactor: 1.5, // Exponential backoff multiplier (default: 1.5) 150 + maxReconnectDelay: 30000 // Maximum delay between attempts (default: 30000ms) 151 + }); 152 + ``` 153 + 154 + All new configuration options are optional and have sensible defaults.
+223 -27
dist/index.d.mts
··· 19 19 service: string; 20 20 }; 21 21 type ActionBot = Bot & { 22 - action: (agent: AtpAgent, params?: any) => Promise<void>; 22 + action: (agent: AtpAgent, params?: unknown) => Promise<void>; 23 23 }; 24 24 type CronBot = ActionBot & { 25 25 cronJob: Cron; ··· 73 73 collection: string; 74 74 rkey: string; 75 75 record: { 76 - '$type': string; 76 + $type: string; 77 77 createdAt: string; 78 78 subject: string; 79 79 reply?: { ··· 90 90 opts: AtpAgentOptions; 91 91 actionBot: ActionBot; 92 92 constructor(opts: AtpAgentOptions, actionBot: ActionBot); 93 - doAction(params: any): Promise<void>; 93 + doAction(params?: unknown): Promise<void>; 94 94 } 95 95 declare const useActionBotAgent: (actionBot: ActionBot) => Promise<ActionBotAgent | null>; 96 96 ··· 121 121 122 122 interface WebSocketClientOptions { 123 123 /** The URL of the WebSocket server to connect to. */ 124 - url: string; 124 + service: string | string[]; 125 125 /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */ 126 126 reconnectInterval?: number; 127 127 /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */ 128 128 pingInterval?: number; 129 + /** Maximum number of consecutive reconnection attempts per service. Default is 3. */ 130 + maxReconnectAttempts?: number; 131 + /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */ 132 + maxReconnectDelay?: number; 133 + /** Exponential backoff factor for reconnection delays. Default is 1.5. */ 134 + backoffFactor?: number; 135 + /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */ 136 + maxServiceCycles?: number; 129 137 } 130 138 /** 131 139 * A WebSocket client that automatically attempts to reconnect upon disconnection ··· 135 143 * to implement custom handling of WebSocket events. 136 144 */ 137 145 declare class WebSocketClient { 138 - private url; 146 + private service; 139 147 private reconnectInterval; 140 148 private pingInterval; 141 149 private ws; 142 150 private pingTimeout; 151 + private serviceIndex; 152 + private reconnectAttempts; 153 + private serviceCycles; 154 + private maxReconnectAttempts; 155 + private maxServiceCycles; 156 + private maxReconnectDelay; 157 + private backoffFactor; 158 + private reconnectTimeout; 159 + private isConnecting; 160 + private shouldReconnect; 161 + private messageCount; 162 + private lastMessageTime; 163 + private healthCheckName; 143 164 /** 144 165 * Creates a new instance of `WebSocketClient`. 145 166 * ··· 158 179 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 159 180 * It clears all event listeners on the old WebSocket and initiates a new connection. 160 181 */ 161 - private reconnect; 182 + private scheduleReconnect; 183 + /** 184 + * Check if we should try the next service in the array. 185 + */ 186 + private shouldTryNextService; 187 + /** 188 + * Move to the next service in the array and reset reconnection attempts. 189 + */ 190 + private moveToNextService; 191 + private cleanup; 162 192 /** 163 193 * Starts sending periodic ping messages to the server. 164 194 * ··· 183 213 * 184 214 * Override this method in a subclass to implement custom message handling. 185 215 */ 186 - protected onMessage(data: WebSocket.Data): void; 216 + protected onMessage(_data: WebSocket.Data): void; 187 217 /** 188 218 * Called when a WebSocket error occurs. 189 219 * 190 220 * @param error - The error that occurred. 191 221 * 192 222 * Override this method in a subclass to implement custom error handling. 223 + * Note: Service switching is now handled in the reconnection logic, not here. 193 224 */ 194 - protected onError(error: Error): void; 225 + protected onError(_error: Error): void; 195 226 /** 196 227 * Called when the WebSocket connection is closed. 197 228 * ··· 203 234 * 204 235 * @param data - The data to send. 205 236 */ 206 - send(data: any): void; 237 + send(data: string | Buffer | ArrayBuffer | Buffer[]): void; 207 238 /** 208 239 * Closes the WebSocket connection gracefully. 209 240 */ 210 241 close(): void; 242 + getConnectionState(): string; 243 + getReconnectAttempts(): number; 244 + getServiceCycles(): number; 245 + getServiceIndex(): number; 246 + getAllServices(): string[]; 247 + getCurrentService(): string; 248 + getMessageCount(): number; 249 + getLastMessageTime(): number; 250 + getHealthCheckName(): string; 211 251 } 212 252 213 253 /** ··· 217 257 * It invokes a provided callback function whenever a message is received from the Jetstream server. 218 258 */ 219 259 declare class JetstreamSubscription extends WebSocketClient { 220 - service: string; 221 260 interval: number; 222 261 private onMessageCallback?; 223 262 /** 224 263 * Creates a new `JetstreamSubscription`. 225 264 * 226 - * @param service - The URL of the Jetstream server to connect to. 265 + * @param service - The URL(-Array) of the Jetstream server(s) to connect to. 227 266 * @param interval - The interval (in milliseconds) for reconnect attempts. 228 267 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 229 268 */ 230 - constructor(service: string, interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined); 269 + constructor(service: string | string[], interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined); 231 270 /** 232 271 * Called when the WebSocket connection is successfully opened. 233 272 * Logs a message indicating that the connection to the Jetstream server has been established. ··· 255 294 protected onClose(): void; 256 295 } 257 296 297 + declare enum LogLevel { 298 + DEBUG = 0, 299 + INFO = 1, 300 + WARN = 2, 301 + ERROR = 3 302 + } 303 + interface LogContext { 304 + correlationId?: string; 305 + botId?: string; 306 + operation?: string; 307 + duration?: number; 308 + [key: string]: unknown; 309 + } 258 310 /** 259 - * A simple logging utility class providing static methods for various log levels. 311 + * A performance-optimized logging utility class providing static methods for various log levels. 260 312 * Each log message is prefixed with a timestamp and log level. 313 + * Supports conditional logging based on log levels and configurable timezone. 261 314 */ 262 315 declare class Logger { 316 + private static logLevel; 317 + private static timezone; 318 + private static correlationId; 319 + /** 320 + * Generate a new correlation ID for tracking related operations. 321 + */ 322 + static generateCorrelationId(): string; 323 + /** 324 + * Set the correlation ID for subsequent log entries. 325 + * @param id - The correlation ID to use, or null to generate a new one 326 + */ 327 + static setCorrelationId(id?: string | null): void; 328 + /** 329 + * Get the current correlation ID. 330 + */ 331 + static getCorrelationId(): string | null; 332 + /** 333 + * Clear the current correlation ID. 334 + */ 335 + static clearCorrelationId(): void; 336 + /** 337 + * Set the minimum log level. Messages below this level will not be logged. 338 + * @param level - The minimum log level 339 + */ 340 + static setLogLevel(level: LogLevel): void; 341 + /** 342 + * Set the timezone for log timestamps. 343 + * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC") 344 + */ 345 + static setTimezone(timezone: string): void; 346 + /** 347 + * Get the current log level. 348 + */ 349 + static getLogLevel(): LogLevel; 350 + /** 351 + * Generate a formatted timestamp string. 352 + * @private 353 + */ 354 + private static getTimestamp; 355 + /** 356 + * Internal logging method that checks log level before processing. 357 + * @private 358 + */ 359 + private static log; 263 360 /** 264 361 * Logs an informational message to the console. 265 362 * 266 363 * @param message - The message to be logged. 267 - * @param context - Optional additional context (object or string) to log alongside the message. 364 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 268 365 */ 269 - static info(message: string, context?: object | string): void; 366 + static info(message: string, context?: LogContext | object | string): void; 270 367 /** 271 368 * Logs a warning message to the console. 272 369 * 273 370 * @param message - The message to be logged. 274 - * @param context - Optional additional context (object or string) to log alongside the message. 371 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 275 372 */ 276 - static warn(message: string, context?: object | string): void; 373 + static warn(message: string, context?: LogContext | object | string): void; 277 374 /** 278 375 * Logs an error message to the console. 279 376 * 280 377 * @param message - The message to be logged. 281 - * @param context - Optional additional context (object or string) to log alongside the message. 378 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 282 379 */ 283 - static error(message: string, context?: object | string): void; 380 + static error(message: string, context?: LogContext | object | string): void; 284 381 /** 285 382 * Logs a debug message to the console. 286 383 * 287 384 * @param message - The message to be logged. 288 - * @param context - Optional additional context (object or string) to log alongside the message. 385 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 289 386 */ 290 - static debug(message: string, context?: object | string): void; 387 + static debug(message: string, context?: LogContext | object | string): void; 388 + /** 389 + * Log operation start with timing. 390 + * @param operation - The operation name 391 + * @param context - Additional context 392 + */ 393 + static startOperation(operation: string, context?: LogContext): string; 394 + /** 395 + * Log operation completion with timing. 396 + * @param operation - The operation name 397 + * @param startTime - The start time from Date.now() 398 + * @param context - Additional context 399 + */ 400 + static endOperation(operation: string, startTime: number, context?: LogContext): void; 291 401 } 292 402 293 403 /** ··· 298 408 */ 299 409 declare const maybeStr: (val?: string) => string | undefined; 300 410 /** 301 - * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 302 - * 303 - * @param val - The optional string value to parse. 304 - * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 305 - */ 411 + * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 412 + * 413 + * @param val - The optional string value to parse. 414 + * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 415 + */ 306 416 declare const maybeInt: (val?: string) => number | undefined; 307 417 308 418 /** ··· 317 427 */ 318 428 declare function websocketToFeedEntry(data: WebSocket.Data): Post | null; 319 429 320 - 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 }; 430 + interface HealthStatus { 431 + healthy: boolean; 432 + timestamp: number; 433 + checks: Record<string, boolean>; 434 + metrics: Record<string, number>; 435 + details?: Record<string, unknown>; 436 + } 437 + interface HealthCheckOptions { 438 + interval?: number; 439 + timeout?: number; 440 + retries?: number; 441 + } 442 + /** 443 + * Health monitoring system for bot components. 444 + * Provides health checks and basic metrics collection. 445 + */ 446 + declare class HealthMonitor { 447 + private checks; 448 + private metrics; 449 + private lastCheckResults; 450 + private checkInterval; 451 + private options; 452 + constructor(options?: HealthCheckOptions); 453 + /** 454 + * Register a health check function. 455 + * @param name - Unique name for the health check 456 + * @param checkFn - Function that returns true if healthy 457 + */ 458 + registerHealthCheck(name: string, checkFn: () => Promise<boolean>): void; 459 + /** 460 + * Remove a health check. 461 + * @param name - Name of the health check to remove 462 + */ 463 + unregisterHealthCheck(name: string): void; 464 + /** 465 + * Set a metric value. 466 + * @param name - Metric name 467 + * @param value - Metric value 468 + */ 469 + setMetric(name: string, value: number): void; 470 + /** 471 + * Increment a counter metric. 472 + * @param name - Metric name 473 + * @param increment - Value to add (default: 1) 474 + */ 475 + incrementMetric(name: string, increment?: number): void; 476 + /** 477 + * Get current metric value. 478 + * @param name - Metric name 479 + * @returns Current value or 0 if not found 480 + */ 481 + getMetric(name: string): number; 482 + /** 483 + * Get all current metrics. 484 + * @returns Object with all metrics 485 + */ 486 + getAllMetrics(): Record<string, number>; 487 + /** 488 + * Run a single health check with timeout and retries. 489 + * @private 490 + */ 491 + private runHealthCheck; 492 + /** 493 + * Wrap a promise with a timeout. 494 + * @private 495 + */ 496 + private withTimeout; 497 + /** 498 + * Run all health checks and return the current health status. 499 + */ 500 + getHealthStatus(): Promise<HealthStatus>; 501 + /** 502 + * Start periodic health monitoring. 503 + */ 504 + start(): void; 505 + /** 506 + * Stop periodic health monitoring. 507 + */ 508 + stop(): void; 509 + /** 510 + * Get a summary of the last health check results. 511 + */ 512 + getLastCheckSummary(): Record<string, boolean>; 513 + } 514 + declare const healthMonitor: HealthMonitor; 515 + 516 + 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
··· 19 19 service: string; 20 20 }; 21 21 type ActionBot = Bot & { 22 - action: (agent: AtpAgent, params?: any) => Promise<void>; 22 + action: (agent: AtpAgent, params?: unknown) => Promise<void>; 23 23 }; 24 24 type CronBot = ActionBot & { 25 25 cronJob: Cron; ··· 73 73 collection: string; 74 74 rkey: string; 75 75 record: { 76 - '$type': string; 76 + $type: string; 77 77 createdAt: string; 78 78 subject: string; 79 79 reply?: { ··· 90 90 opts: AtpAgentOptions; 91 91 actionBot: ActionBot; 92 92 constructor(opts: AtpAgentOptions, actionBot: ActionBot); 93 - doAction(params: any): Promise<void>; 93 + doAction(params?: unknown): Promise<void>; 94 94 } 95 95 declare const useActionBotAgent: (actionBot: ActionBot) => Promise<ActionBotAgent | null>; 96 96 ··· 121 121 122 122 interface WebSocketClientOptions { 123 123 /** The URL of the WebSocket server to connect to. */ 124 - url: string; 124 + service: string | string[]; 125 125 /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */ 126 126 reconnectInterval?: number; 127 127 /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */ 128 128 pingInterval?: number; 129 + /** Maximum number of consecutive reconnection attempts per service. Default is 3. */ 130 + maxReconnectAttempts?: number; 131 + /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */ 132 + maxReconnectDelay?: number; 133 + /** Exponential backoff factor for reconnection delays. Default is 1.5. */ 134 + backoffFactor?: number; 135 + /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */ 136 + maxServiceCycles?: number; 129 137 } 130 138 /** 131 139 * A WebSocket client that automatically attempts to reconnect upon disconnection ··· 135 143 * to implement custom handling of WebSocket events. 136 144 */ 137 145 declare class WebSocketClient { 138 - private url; 146 + private service; 139 147 private reconnectInterval; 140 148 private pingInterval; 141 149 private ws; 142 150 private pingTimeout; 151 + private serviceIndex; 152 + private reconnectAttempts; 153 + private serviceCycles; 154 + private maxReconnectAttempts; 155 + private maxServiceCycles; 156 + private maxReconnectDelay; 157 + private backoffFactor; 158 + private reconnectTimeout; 159 + private isConnecting; 160 + private shouldReconnect; 161 + private messageCount; 162 + private lastMessageTime; 163 + private healthCheckName; 143 164 /** 144 165 * Creates a new instance of `WebSocketClient`. 145 166 * ··· 158 179 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 159 180 * It clears all event listeners on the old WebSocket and initiates a new connection. 160 181 */ 161 - private reconnect; 182 + private scheduleReconnect; 183 + /** 184 + * Check if we should try the next service in the array. 185 + */ 186 + private shouldTryNextService; 187 + /** 188 + * Move to the next service in the array and reset reconnection attempts. 189 + */ 190 + private moveToNextService; 191 + private cleanup; 162 192 /** 163 193 * Starts sending periodic ping messages to the server. 164 194 * ··· 183 213 * 184 214 * Override this method in a subclass to implement custom message handling. 185 215 */ 186 - protected onMessage(data: WebSocket.Data): void; 216 + protected onMessage(_data: WebSocket.Data): void; 187 217 /** 188 218 * Called when a WebSocket error occurs. 189 219 * 190 220 * @param error - The error that occurred. 191 221 * 192 222 * Override this method in a subclass to implement custom error handling. 223 + * Note: Service switching is now handled in the reconnection logic, not here. 193 224 */ 194 - protected onError(error: Error): void; 225 + protected onError(_error: Error): void; 195 226 /** 196 227 * Called when the WebSocket connection is closed. 197 228 * ··· 203 234 * 204 235 * @param data - The data to send. 205 236 */ 206 - send(data: any): void; 237 + send(data: string | Buffer | ArrayBuffer | Buffer[]): void; 207 238 /** 208 239 * Closes the WebSocket connection gracefully. 209 240 */ 210 241 close(): void; 242 + getConnectionState(): string; 243 + getReconnectAttempts(): number; 244 + getServiceCycles(): number; 245 + getServiceIndex(): number; 246 + getAllServices(): string[]; 247 + getCurrentService(): string; 248 + getMessageCount(): number; 249 + getLastMessageTime(): number; 250 + getHealthCheckName(): string; 211 251 } 212 252 213 253 /** ··· 217 257 * It invokes a provided callback function whenever a message is received from the Jetstream server. 218 258 */ 219 259 declare class JetstreamSubscription extends WebSocketClient { 220 - service: string; 221 260 interval: number; 222 261 private onMessageCallback?; 223 262 /** 224 263 * Creates a new `JetstreamSubscription`. 225 264 * 226 - * @param service - The URL of the Jetstream server to connect to. 265 + * @param service - The URL(-Array) of the Jetstream server(s) to connect to. 227 266 * @param interval - The interval (in milliseconds) for reconnect attempts. 228 267 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 229 268 */ 230 - constructor(service: string, interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined); 269 + constructor(service: string | string[], interval: number, onMessageCallback?: ((data: WebSocket.Data) => void) | undefined); 231 270 /** 232 271 * Called when the WebSocket connection is successfully opened. 233 272 * Logs a message indicating that the connection to the Jetstream server has been established. ··· 255 294 protected onClose(): void; 256 295 } 257 296 297 + declare enum LogLevel { 298 + DEBUG = 0, 299 + INFO = 1, 300 + WARN = 2, 301 + ERROR = 3 302 + } 303 + interface LogContext { 304 + correlationId?: string; 305 + botId?: string; 306 + operation?: string; 307 + duration?: number; 308 + [key: string]: unknown; 309 + } 258 310 /** 259 - * A simple logging utility class providing static methods for various log levels. 311 + * A performance-optimized logging utility class providing static methods for various log levels. 260 312 * Each log message is prefixed with a timestamp and log level. 313 + * Supports conditional logging based on log levels and configurable timezone. 261 314 */ 262 315 declare class Logger { 316 + private static logLevel; 317 + private static timezone; 318 + private static correlationId; 319 + /** 320 + * Generate a new correlation ID for tracking related operations. 321 + */ 322 + static generateCorrelationId(): string; 323 + /** 324 + * Set the correlation ID for subsequent log entries. 325 + * @param id - The correlation ID to use, or null to generate a new one 326 + */ 327 + static setCorrelationId(id?: string | null): void; 328 + /** 329 + * Get the current correlation ID. 330 + */ 331 + static getCorrelationId(): string | null; 332 + /** 333 + * Clear the current correlation ID. 334 + */ 335 + static clearCorrelationId(): void; 336 + /** 337 + * Set the minimum log level. Messages below this level will not be logged. 338 + * @param level - The minimum log level 339 + */ 340 + static setLogLevel(level: LogLevel): void; 341 + /** 342 + * Set the timezone for log timestamps. 343 + * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC") 344 + */ 345 + static setTimezone(timezone: string): void; 346 + /** 347 + * Get the current log level. 348 + */ 349 + static getLogLevel(): LogLevel; 350 + /** 351 + * Generate a formatted timestamp string. 352 + * @private 353 + */ 354 + private static getTimestamp; 355 + /** 356 + * Internal logging method that checks log level before processing. 357 + * @private 358 + */ 359 + private static log; 263 360 /** 264 361 * Logs an informational message to the console. 265 362 * 266 363 * @param message - The message to be logged. 267 - * @param context - Optional additional context (object or string) to log alongside the message. 364 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 268 365 */ 269 - static info(message: string, context?: object | string): void; 366 + static info(message: string, context?: LogContext | object | string): void; 270 367 /** 271 368 * Logs a warning message to the console. 272 369 * 273 370 * @param message - The message to be logged. 274 - * @param context - Optional additional context (object or string) to log alongside the message. 371 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 275 372 */ 276 - static warn(message: string, context?: object | string): void; 373 + static warn(message: string, context?: LogContext | object | string): void; 277 374 /** 278 375 * Logs an error message to the console. 279 376 * 280 377 * @param message - The message to be logged. 281 - * @param context - Optional additional context (object or string) to log alongside the message. 378 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 282 379 */ 283 - static error(message: string, context?: object | string): void; 380 + static error(message: string, context?: LogContext | object | string): void; 284 381 /** 285 382 * Logs a debug message to the console. 286 383 * 287 384 * @param message - The message to be logged. 288 - * @param context - Optional additional context (object or string) to log alongside the message. 385 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 289 386 */ 290 - static debug(message: string, context?: object | string): void; 387 + static debug(message: string, context?: LogContext | object | string): void; 388 + /** 389 + * Log operation start with timing. 390 + * @param operation - The operation name 391 + * @param context - Additional context 392 + */ 393 + static startOperation(operation: string, context?: LogContext): string; 394 + /** 395 + * Log operation completion with timing. 396 + * @param operation - The operation name 397 + * @param startTime - The start time from Date.now() 398 + * @param context - Additional context 399 + */ 400 + static endOperation(operation: string, startTime: number, context?: LogContext): void; 291 401 } 292 402 293 403 /** ··· 298 408 */ 299 409 declare const maybeStr: (val?: string) => string | undefined; 300 410 /** 301 - * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 302 - * 303 - * @param val - The optional string value to parse. 304 - * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 305 - */ 411 + * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 412 + * 413 + * @param val - The optional string value to parse. 414 + * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 415 + */ 306 416 declare const maybeInt: (val?: string) => number | undefined; 307 417 308 418 /** ··· 317 427 */ 318 428 declare function websocketToFeedEntry(data: WebSocket.Data): Post | null; 319 429 320 - 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 }; 430 + interface HealthStatus { 431 + healthy: boolean; 432 + timestamp: number; 433 + checks: Record<string, boolean>; 434 + metrics: Record<string, number>; 435 + details?: Record<string, unknown>; 436 + } 437 + interface HealthCheckOptions { 438 + interval?: number; 439 + timeout?: number; 440 + retries?: number; 441 + } 442 + /** 443 + * Health monitoring system for bot components. 444 + * Provides health checks and basic metrics collection. 445 + */ 446 + declare class HealthMonitor { 447 + private checks; 448 + private metrics; 449 + private lastCheckResults; 450 + private checkInterval; 451 + private options; 452 + constructor(options?: HealthCheckOptions); 453 + /** 454 + * Register a health check function. 455 + * @param name - Unique name for the health check 456 + * @param checkFn - Function that returns true if healthy 457 + */ 458 + registerHealthCheck(name: string, checkFn: () => Promise<boolean>): void; 459 + /** 460 + * Remove a health check. 461 + * @param name - Name of the health check to remove 462 + */ 463 + unregisterHealthCheck(name: string): void; 464 + /** 465 + * Set a metric value. 466 + * @param name - Metric name 467 + * @param value - Metric value 468 + */ 469 + setMetric(name: string, value: number): void; 470 + /** 471 + * Increment a counter metric. 472 + * @param name - Metric name 473 + * @param increment - Value to add (default: 1) 474 + */ 475 + incrementMetric(name: string, increment?: number): void; 476 + /** 477 + * Get current metric value. 478 + * @param name - Metric name 479 + * @returns Current value or 0 if not found 480 + */ 481 + getMetric(name: string): number; 482 + /** 483 + * Get all current metrics. 484 + * @returns Object with all metrics 485 + */ 486 + getAllMetrics(): Record<string, number>; 487 + /** 488 + * Run a single health check with timeout and retries. 489 + * @private 490 + */ 491 + private runHealthCheck; 492 + /** 493 + * Wrap a promise with a timeout. 494 + * @private 495 + */ 496 + private withTimeout; 497 + /** 498 + * Run all health checks and return the current health status. 499 + */ 500 + getHealthStatus(): Promise<HealthStatus>; 501 + /** 502 + * Start periodic health monitoring. 503 + */ 504 + start(): void; 505 + /** 506 + * Stop periodic health monitoring. 507 + */ 508 + stop(): void; 509 + /** 510 + * Get a summary of the last health check results. 511 + */ 512 + getLastCheckSummary(): Record<string, boolean>; 513 + } 514 + declare const healthMonitor: HealthMonitor; 515 + 516 + 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
··· 3 3 var __defProp = Object.defineProperty; 4 4 var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 5 5 var __getOwnPropNames = Object.getOwnPropertyNames; 6 + var __getOwnPropSymbols = Object.getOwnPropertySymbols; 6 7 var __getProtoOf = Object.getPrototypeOf; 7 8 var __hasOwnProp = Object.prototype.hasOwnProperty; 9 + var __propIsEnum = Object.prototype.propertyIsEnumerable; 10 + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 11 + var __spreadValues = (a, b) => { 12 + for (var prop in b || (b = {})) 13 + if (__hasOwnProp.call(b, prop)) 14 + __defNormalProp(a, prop, b[prop]); 15 + if (__getOwnPropSymbols) 16 + for (var prop of __getOwnPropSymbols(b)) { 17 + if (__propIsEnum.call(b, prop)) 18 + __defNormalProp(a, prop, b[prop]); 19 + } 20 + return a; 21 + }; 8 22 var __export = (target, all) => { 9 23 for (var name in all) 10 24 __defProp(target, name, { get: all[name], enumerable: true }); ··· 52 66 __export(index_exports, { 53 67 ActionBotAgent: () => ActionBotAgent, 54 68 CronBotAgent: () => CronBotAgent, 69 + HealthMonitor: () => HealthMonitor, 55 70 JetstreamSubscription: () => JetstreamSubscription, 56 71 KeywordBotAgent: () => KeywordBotAgent, 72 + LogLevel: () => LogLevel, 57 73 Logger: () => Logger, 58 74 WebSocketClient: () => WebSocketClient, 59 75 buildReplyToPost: () => buildReplyToPost, 60 76 filterBotReplies: () => filterBotReplies, 77 + healthMonitor: () => healthMonitor, 61 78 maybeInt: () => maybeInt, 62 79 maybeStr: () => maybeStr, 63 80 useActionBotAgent: () => useActionBotAgent, ··· 71 88 var import_api = require("@atproto/api"); 72 89 73 90 // src/utils/logger.ts 91 + var LogLevel = /* @__PURE__ */ ((LogLevel2) => { 92 + LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG"; 93 + LogLevel2[LogLevel2["INFO"] = 1] = "INFO"; 94 + LogLevel2[LogLevel2["WARN"] = 2] = "WARN"; 95 + LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR"; 96 + return LogLevel2; 97 + })(LogLevel || {}); 74 98 var Logger = class { 75 99 /** 100 + * Generate a new correlation ID for tracking related operations. 101 + */ 102 + static generateCorrelationId() { 103 + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 104 + } 105 + /** 106 + * Set the correlation ID for subsequent log entries. 107 + * @param id - The correlation ID to use, or null to generate a new one 108 + */ 109 + static setCorrelationId(id) { 110 + this.correlationId = id || this.generateCorrelationId(); 111 + } 112 + /** 113 + * Get the current correlation ID. 114 + */ 115 + static getCorrelationId() { 116 + return this.correlationId; 117 + } 118 + /** 119 + * Clear the current correlation ID. 120 + */ 121 + static clearCorrelationId() { 122 + this.correlationId = null; 123 + } 124 + /** 125 + * Set the minimum log level. Messages below this level will not be logged. 126 + * @param level - The minimum log level 127 + */ 128 + static setLogLevel(level) { 129 + this.logLevel = level; 130 + } 131 + /** 132 + * Set the timezone for log timestamps. 133 + * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC") 134 + */ 135 + static setTimezone(timezone) { 136 + this.timezone = timezone; 137 + } 138 + /** 139 + * Get the current log level. 140 + */ 141 + static getLogLevel() { 142 + return this.logLevel; 143 + } 144 + /** 145 + * Generate a formatted timestamp string. 146 + * @private 147 + */ 148 + static getTimestamp() { 149 + return (/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: this.timezone }); 150 + } 151 + /** 152 + * Internal logging method that checks log level before processing. 153 + * @private 154 + */ 155 + static log(level, levelName, message, context, logFn = console.log) { 156 + if (level < this.logLevel) { 157 + return; 158 + } 159 + const timestamp = this.getTimestamp(); 160 + let formattedMessage = `${timestamp} [${levelName}]`; 161 + if (this.correlationId) { 162 + formattedMessage += ` [${this.correlationId}]`; 163 + } 164 + if (context && typeof context === "object" && "correlationId" in context && context.correlationId && context.correlationId !== this.correlationId) { 165 + formattedMessage += ` [${context.correlationId}]`; 166 + } 167 + formattedMessage += `: ${message}`; 168 + if (context) { 169 + if (typeof context === "object") { 170 + const logEntry = __spreadValues({ 171 + timestamp: (/* @__PURE__ */ new Date()).toISOString(), 172 + level: levelName, 173 + message, 174 + correlationId: this.correlationId 175 + }, context); 176 + logFn(formattedMessage, logEntry); 177 + } else { 178 + logFn(formattedMessage, context); 179 + } 180 + } else { 181 + logFn(formattedMessage); 182 + } 183 + } 184 + /** 76 185 * Logs an informational message to the console. 77 186 * 78 187 * @param message - The message to be logged. 79 - * @param context - Optional additional context (object or string) to log alongside the message. 188 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 80 189 */ 81 190 static info(message, context) { 82 - console.info(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [INFO]: ${message}`, context || ""); 191 + this.log(1 /* INFO */, "INFO", message, context, console.info); 83 192 } 84 193 /** 85 194 * Logs a warning message to the console. 86 195 * 87 196 * @param message - The message to be logged. 88 - * @param context - Optional additional context (object or string) to log alongside the message. 197 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 89 198 */ 90 199 static warn(message, context) { 91 - console.warn(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [WARNING]: ${message}`, context || ""); 200 + this.log(2 /* WARN */, "WARNING", message, context, console.warn); 92 201 } 93 202 /** 94 203 * Logs an error message to the console. 95 204 * 96 205 * @param message - The message to be logged. 97 - * @param context - Optional additional context (object or string) to log alongside the message. 206 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 98 207 */ 99 208 static error(message, context) { 100 - console.error(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [ERROR]: ${message}`, context || ""); 209 + this.log(3 /* ERROR */, "ERROR", message, context, console.error); 101 210 } 102 211 /** 103 212 * Logs a debug message to the console. 104 213 * 105 214 * @param message - The message to be logged. 106 - * @param context - Optional additional context (object or string) to log alongside the message. 215 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 107 216 */ 108 217 static debug(message, context) { 109 - console.debug(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [DEBUG]: ${message}`, context || ""); 218 + this.log(0 /* DEBUG */, "DEBUG", message, context, console.debug); 219 + } 220 + /** 221 + * Log operation start with timing. 222 + * @param operation - The operation name 223 + * @param context - Additional context 224 + */ 225 + static startOperation(operation, context) { 226 + const correlationId = (context == null ? void 0 : context.correlationId) || this.generateCorrelationId(); 227 + this.setCorrelationId(correlationId); 228 + this.info(`Starting operation: ${operation}`, __spreadValues({ 229 + operation, 230 + correlationId 231 + }, context)); 232 + return correlationId; 233 + } 234 + /** 235 + * Log operation completion with timing. 236 + * @param operation - The operation name 237 + * @param startTime - The start time from Date.now() 238 + * @param context - Additional context 239 + */ 240 + static endOperation(operation, startTime, context) { 241 + const duration = Date.now() - startTime; 242 + this.info(`Completed operation: ${operation}`, __spreadValues({ 243 + operation, 244 + duration: `${duration}ms` 245 + }, context)); 110 246 } 111 247 }; 248 + Logger.logLevel = 1 /* INFO */; 249 + Logger.timezone = "Europe/Vienna"; 250 + Logger.correlationId = null; 112 251 113 252 // src/bots/actionBot.ts 114 253 var ActionBotAgent = class extends import_api.AtpAgent { ··· 119 258 } 120 259 doAction(params) { 121 260 return __async(this, null, function* () { 122 - this.actionBot.action(this, params); 261 + const correlationId = Logger.startOperation("actionBot.doAction", { 262 + botId: this.actionBot.username || this.actionBot.identifier 263 + }); 264 + const startTime = Date.now(); 265 + try { 266 + yield this.actionBot.action(this, params); 267 + Logger.endOperation("actionBot.doAction", startTime, { 268 + correlationId, 269 + botId: this.actionBot.username || this.actionBot.identifier 270 + }); 271 + } catch (error) { 272 + Logger.error("Action bot execution failed", { 273 + correlationId, 274 + botId: this.actionBot.username || this.actionBot.identifier, 275 + error: error instanceof Error ? error.message : String(error) 276 + }); 277 + throw error; 278 + } 123 279 }); 124 280 } 125 281 }; 126 282 var useActionBotAgent = (actionBot) => __async(void 0, null, function* () { 127 - var _a, _b, _c; 283 + var _a; 284 + const botId = (_a = actionBot.username) != null ? _a : actionBot.identifier; 285 + const correlationId = Logger.startOperation("initializeActionBot", { botId }); 286 + const startTime = Date.now(); 128 287 const agent = new ActionBotAgent({ service: actionBot.service }, actionBot); 129 288 try { 130 - Logger.info(`Initialize action bot ${(_a = actionBot.username) != null ? _a : actionBot.identifier}`); 131 - const login = yield agent.login({ identifier: actionBot.identifier, password: actionBot.password }); 289 + Logger.info("Initializing action bot", { correlationId, botId }); 290 + const login = yield agent.login({ 291 + identifier: actionBot.identifier, 292 + password: actionBot.password 293 + }); 132 294 if (!login.success) { 133 - Logger.warn(`Failed to login action bot ${(_b = actionBot.username) != null ? _b : actionBot.identifier}`); 295 + Logger.warn("Action bot login failed", { correlationId, botId }); 134 296 return null; 135 297 } 298 + Logger.endOperation("initializeActionBot", startTime, { correlationId, botId }); 136 299 return agent; 137 300 } catch (error) { 138 - Logger.error("Failed to initialize action bot:", `${error}, ${(_c = actionBot.username) != null ? _c : actionBot.identifier}`); 301 + Logger.error("Failed to initialize action bot", { 302 + correlationId, 303 + botId, 304 + error: error.message, 305 + duration: Date.now() - startTime 306 + }); 139 307 return null; 140 308 } 141 309 }); ··· 164 332 const agent = new CronBotAgent({ service: cronBot.service }, cronBot); 165 333 try { 166 334 Logger.info(`Initialize cron bot ${(_a = cronBot.username) != null ? _a : cronBot.identifier}`); 167 - const login = yield agent.login({ identifier: cronBot.identifier, password: cronBot.password }); 335 + const login = yield agent.login({ 336 + identifier: cronBot.identifier, 337 + password: cronBot.password 338 + }); 168 339 if (!login.success) { 169 340 Logger.info(`Failed to login cron bot ${(_b = cronBot.username) != null ? _b : cronBot.identifier}`); 170 341 return null; ··· 172 343 agent.job.start(); 173 344 return agent; 174 345 } catch (error) { 175 - Logger.error("Failed to initialize cron bot:", `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`); 346 + Logger.error( 347 + "Failed to initialize cron bot:", 348 + `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}` 349 + ); 176 350 return null; 177 351 } 178 352 }); ··· 209 383 message 210 384 ); 211 385 yield Promise.all([this.like(post.uri, post.cid), this.post(reply)]); 212 - Logger.info(`Replied to post: ${post.uri}`, (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier); 386 + Logger.info( 387 + `Replied to post: ${post.uri}`, 388 + (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier 389 + ); 213 390 } 214 391 } catch (error) { 215 - Logger.error("Error while replying:", `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`); 392 + Logger.error( 393 + "Error while replying:", 394 + `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}` 395 + ); 216 396 } 217 397 }); 218 398 } ··· 222 402 $type: "app.bsky.feed.post", 223 403 text: message, 224 404 reply: { 225 - "root": root, 226 - "parent": parent 405 + root, 406 + parent 227 407 } 228 408 }; 229 409 } 230 410 function filterBotReplies(text, botReplies) { 411 + const lowerText = text.toLowerCase(); 231 412 return botReplies.filter((reply) => { 232 413 const keyword = reply.keyword.toLowerCase(); 233 - const keywordFound = text.toLowerCase().includes(keyword); 234 - if (!keywordFound) { 414 + if (!lowerText.includes(keyword)) { 235 415 return false; 236 416 } 237 - if (Array.isArray(reply.exclude) && reply.exclude.length > 0) { 238 - for (const excludeWord of reply.exclude) { 239 - if (text.toLowerCase().includes(excludeWord.toLowerCase())) { 240 - return false; 241 - } 242 - } 417 + if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) { 418 + return true; 243 419 } 244 - return true; 420 + const hasExcludedWord = reply.exclude.some( 421 + (excludeWord) => lowerText.includes(excludeWord.toLowerCase()) 422 + ); 423 + return !hasExcludedWord; 245 424 }); 246 425 } 247 426 var useKeywordBotAgent = (keywordBot) => __async(void 0, null, function* () { 248 427 var _a, _b, _c; 249 428 const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot); 250 429 try { 251 - const login = yield agent.login({ identifier: keywordBot.identifier, password: keywordBot.password }); 430 + const login = yield agent.login({ 431 + identifier: keywordBot.identifier, 432 + password: keywordBot.password 433 + }); 252 434 Logger.info(`Initialize keyword bot ${(_a = keywordBot.username) != null ? _a : keywordBot.identifier}`); 253 435 if (!login.success) { 254 436 Logger.warn(`Failed to login keyword bot ${(_b = keywordBot.username) != null ? _b : keywordBot.identifier}`); ··· 256 438 } 257 439 return agent; 258 440 } catch (error) { 259 - Logger.error("Failed to initialize keyword bot:", `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`); 441 + Logger.error( 442 + "Failed to initialize keyword bot:", 443 + `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}` 444 + ); 260 445 return null; 261 446 } 262 447 }); 263 448 264 449 // src/utils/websocketClient.ts 265 450 var import_ws = __toESM(require("ws")); 451 + 452 + // src/utils/healthCheck.ts 453 + var HealthMonitor = class { 454 + constructor(options = {}) { 455 + this.checks = /* @__PURE__ */ new Map(); 456 + this.metrics = /* @__PURE__ */ new Map(); 457 + this.lastCheckResults = /* @__PURE__ */ new Map(); 458 + this.checkInterval = null; 459 + this.options = { 460 + interval: options.interval || 3e4, 461 + // 30 seconds 462 + timeout: options.timeout || 5e3, 463 + // 5 seconds 464 + retries: options.retries || 2 465 + }; 466 + } 467 + /** 468 + * Register a health check function. 469 + * @param name - Unique name for the health check 470 + * @param checkFn - Function that returns true if healthy 471 + */ 472 + registerHealthCheck(name, checkFn) { 473 + this.checks.set(name, checkFn); 474 + Logger.debug(`Registered health check: ${name}`); 475 + } 476 + /** 477 + * Remove a health check. 478 + * @param name - Name of the health check to remove 479 + */ 480 + unregisterHealthCheck(name) { 481 + this.checks.delete(name); 482 + this.lastCheckResults.delete(name); 483 + Logger.debug(`Unregistered health check: ${name}`); 484 + } 485 + /** 486 + * Set a metric value. 487 + * @param name - Metric name 488 + * @param value - Metric value 489 + */ 490 + setMetric(name, value) { 491 + this.metrics.set(name, value); 492 + } 493 + /** 494 + * Increment a counter metric. 495 + * @param name - Metric name 496 + * @param increment - Value to add (default: 1) 497 + */ 498 + incrementMetric(name, increment = 1) { 499 + const current = this.metrics.get(name) || 0; 500 + this.metrics.set(name, current + increment); 501 + } 502 + /** 503 + * Get current metric value. 504 + * @param name - Metric name 505 + * @returns Current value or 0 if not found 506 + */ 507 + getMetric(name) { 508 + return this.metrics.get(name) || 0; 509 + } 510 + /** 511 + * Get all current metrics. 512 + * @returns Object with all metrics 513 + */ 514 + getAllMetrics() { 515 + return Object.fromEntries(this.metrics); 516 + } 517 + /** 518 + * Run a single health check with timeout and retries. 519 + * @private 520 + */ 521 + runHealthCheck(name, checkFn) { 522 + return __async(this, null, function* () { 523 + for (let attempt = 0; attempt <= this.options.retries; attempt++) { 524 + try { 525 + const result = yield this.withTimeout(checkFn(), this.options.timeout); 526 + if (result) { 527 + return true; 528 + } 529 + } catch (error) { 530 + Logger.debug( 531 + `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`, 532 + { error: error.message } 533 + ); 534 + } 535 + } 536 + return false; 537 + }); 538 + } 539 + /** 540 + * Wrap a promise with a timeout. 541 + * @private 542 + */ 543 + withTimeout(promise, timeoutMs) { 544 + return Promise.race([ 545 + promise, 546 + new Promise( 547 + (_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) 548 + ) 549 + ]); 550 + } 551 + /** 552 + * Run all health checks and return the current health status. 553 + */ 554 + getHealthStatus() { 555 + return __async(this, null, function* () { 556 + const timestamp = Date.now(); 557 + const checkResults = {}; 558 + const details = {}; 559 + const checkPromises = Array.from(this.checks.entries()).map((_0) => __async(this, [_0], function* ([name, checkFn]) { 560 + const result = yield this.runHealthCheck(name, checkFn); 561 + checkResults[name] = result; 562 + this.lastCheckResults.set(name, result); 563 + if (!result) { 564 + details[`${name}_last_failure`] = (/* @__PURE__ */ new Date()).toISOString(); 565 + } 566 + return result; 567 + })); 568 + yield Promise.allSettled(checkPromises); 569 + const healthy = Object.values(checkResults).every((result) => result); 570 + const metrics = this.getAllMetrics(); 571 + return { 572 + healthy, 573 + timestamp, 574 + checks: checkResults, 575 + metrics, 576 + details 577 + }; 578 + }); 579 + } 580 + /** 581 + * Start periodic health monitoring. 582 + */ 583 + start() { 584 + if (this.checkInterval) { 585 + this.stop(); 586 + } 587 + Logger.info(`Starting health monitor with ${this.options.interval}ms interval`); 588 + this.checkInterval = setInterval(() => __async(this, null, function* () { 589 + try { 590 + const status = yield this.getHealthStatus(); 591 + if (!status.healthy) { 592 + const failedChecks = Object.entries(status.checks).filter(([, healthy]) => !healthy).map(([name]) => name); 593 + Logger.warn(`Health check failed`, { 594 + operation: "health_check", 595 + failed_checks: failedChecks, 596 + metrics: status.metrics 597 + }); 598 + } else { 599 + Logger.debug("Health check passed", { 600 + operation: "health_check", 601 + metrics: status.metrics 602 + }); 603 + } 604 + } catch (error) { 605 + Logger.error("Error during health check:", { error: error.message }); 606 + } 607 + }), this.options.interval); 608 + } 609 + /** 610 + * Stop periodic health monitoring. 611 + */ 612 + stop() { 613 + if (this.checkInterval) { 614 + clearInterval(this.checkInterval); 615 + this.checkInterval = null; 616 + Logger.info("Stopped health monitor"); 617 + } 618 + } 619 + /** 620 + * Get a summary of the last health check results. 621 + */ 622 + getLastCheckSummary() { 623 + return Object.fromEntries(this.lastCheckResults); 624 + } 625 + }; 626 + var healthMonitor = new HealthMonitor(); 627 + 628 + // src/utils/websocketClient.ts 266 629 var WebSocketClient = class { 267 630 /** 268 631 * Creates a new instance of `WebSocketClient`. 269 - * 632 + * 270 633 * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval. 271 634 */ 272 635 constructor(options) { 273 636 this.ws = null; 274 637 this.pingTimeout = null; 275 - this.url = options.url; 638 + this.serviceIndex = 0; 639 + this.reconnectAttempts = 0; 640 + this.serviceCycles = 0; 641 + this.reconnectTimeout = null; 642 + this.isConnecting = false; 643 + this.shouldReconnect = true; 644 + this.messageCount = 0; 645 + this.lastMessageTime = 0; 646 + this.service = options.service; 276 647 this.reconnectInterval = options.reconnectInterval || 5e3; 277 648 this.pingInterval = options.pingInterval || 1e4; 649 + this.maxReconnectAttempts = options.maxReconnectAttempts || 3; 650 + this.maxServiceCycles = options.maxServiceCycles || 2; 651 + this.maxReconnectDelay = options.maxReconnectDelay || 3e4; 652 + this.backoffFactor = options.backoffFactor || 1.5; 653 + this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; 654 + healthMonitor.registerHealthCheck(this.healthCheckName, () => __async(this, null, function* () { 655 + return this.getConnectionState() === "CONNECTED"; 656 + })); 657 + healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0); 658 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0); 278 659 this.run(); 279 660 } 280 661 /** 281 662 * Initiates a WebSocket connection to the specified URL. 282 - * 663 + * 283 664 * This method sets up event listeners for `open`, `message`, `error`, and `close` events. 284 665 * When the connection opens, it starts the heartbeat mechanism. 285 666 * On close, it attempts to reconnect after a specified interval. 286 667 */ 287 668 run() { 288 - this.ws = new import_ws.default(this.url); 669 + if (this.isConnecting) { 670 + return; 671 + } 672 + this.isConnecting = true; 673 + const currentService = Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service; 674 + Logger.info(`Attempting to connect to WebSocket: ${currentService}`); 675 + this.ws = new import_ws.default(currentService); 289 676 this.ws.on("open", () => { 290 - Logger.info("WebSocket connected"); 677 + Logger.info("WebSocket connected successfully", { 678 + service: this.getCurrentService(), 679 + serviceIndex: this.serviceIndex 680 + }); 681 + this.isConnecting = false; 682 + this.reconnectAttempts = 0; 683 + this.serviceCycles = 0; 684 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 291 685 this.startHeartbeat(); 292 686 this.onOpen(); 293 687 }); 294 688 this.ws.on("message", (data) => { 689 + this.messageCount++; 690 + this.lastMessageTime = Date.now(); 691 + healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`); 295 692 this.onMessage(data); 296 693 }); 297 694 this.ws.on("error", (error) => { 298 695 Logger.error("WebSocket error:", error); 696 + this.isConnecting = false; 299 697 this.onError(error); 300 698 }); 301 - this.ws.on("close", () => { 302 - Logger.info("WebSocket disconnected"); 699 + this.ws.on("close", (code, reason) => { 700 + Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`); 701 + this.isConnecting = false; 303 702 this.stopHeartbeat(); 304 703 this.onClose(); 305 - this.reconnect(); 704 + if (this.shouldReconnect) { 705 + this.scheduleReconnect(); 706 + } 306 707 }); 307 708 } 308 709 /** 309 710 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 310 711 * It clears all event listeners on the old WebSocket and initiates a new connection. 311 712 */ 312 - reconnect() { 713 + scheduleReconnect() { 714 + this.reconnectAttempts++; 715 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 716 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { 717 + if (this.shouldTryNextService()) { 718 + this.moveToNextService(); 719 + return; 720 + } else { 721 + Logger.error("All services exhausted after maximum cycles", { 722 + totalServices: Array.isArray(this.service) ? this.service.length : 1, 723 + maxServiceCycles: this.maxServiceCycles, 724 + serviceCycles: this.serviceCycles 725 + }); 726 + return; 727 + } 728 + } 729 + const delay = Math.min( 730 + this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1), 731 + this.maxReconnectDelay 732 + ); 733 + Logger.info( 734 + `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`, 735 + { 736 + service: this.getCurrentService(), 737 + serviceIndex: this.serviceIndex, 738 + delay: `${delay}ms` 739 + } 740 + ); 741 + if (this.reconnectTimeout) { 742 + clearTimeout(this.reconnectTimeout); 743 + } 744 + this.reconnectTimeout = setTimeout(() => { 745 + this.cleanup(); 746 + this.run(); 747 + }, delay); 748 + } 749 + /** 750 + * Check if we should try the next service in the array. 751 + */ 752 + shouldTryNextService() { 753 + if (!Array.isArray(this.service)) { 754 + return false; 755 + } 756 + return this.serviceCycles < this.maxServiceCycles; 757 + } 758 + /** 759 + * Move to the next service in the array and reset reconnection attempts. 760 + */ 761 + moveToNextService() { 762 + if (!Array.isArray(this.service)) { 763 + return; 764 + } 765 + const previousIndex = this.serviceIndex; 766 + this.serviceIndex = (this.serviceIndex + 1) % this.service.length; 767 + if (this.serviceIndex === 0) { 768 + this.serviceCycles++; 769 + } 770 + this.reconnectAttempts = 0; 771 + Logger.info("Switching to next service", { 772 + previousService: this.service[previousIndex], 773 + previousIndex, 774 + newService: this.getCurrentService(), 775 + newIndex: this.serviceIndex, 776 + serviceCycle: this.serviceCycles 777 + }); 778 + this.cleanup(); 779 + this.run(); 780 + } 781 + cleanup() { 313 782 if (this.ws) { 314 783 this.ws.removeAllListeners(); 784 + if (this.ws.readyState === import_ws.default.OPEN) { 785 + this.ws.close(); 786 + } 315 787 this.ws = null; 316 788 } 317 - setTimeout(() => this.run(), this.reconnectInterval); 789 + if (this.reconnectTimeout) { 790 + clearTimeout(this.reconnectTimeout); 791 + this.reconnectTimeout = null; 792 + } 318 793 } 319 794 /** 320 795 * Starts sending periodic ping messages to the server. 321 - * 796 + * 322 797 * This function uses `setInterval` to send a ping at the configured `pingInterval`. 323 798 * If the WebSocket is not open, pings are not sent. 324 799 */ ··· 340 815 } 341 816 /** 342 817 * Called when the WebSocket connection is successfully opened. 343 - * 818 + * 344 819 * Override this method in a subclass to implement custom logic on connection. 345 820 */ 346 821 onOpen() { 347 822 } 348 823 /** 349 824 * Called when a WebSocket message is received. 350 - * 825 + * 351 826 * @param data - The data received from the WebSocket server. 352 - * 827 + * 353 828 * Override this method in a subclass to implement custom message handling. 354 829 */ 355 - onMessage(data) { 830 + onMessage(_data) { 356 831 } 357 832 /** 358 833 * Called when a WebSocket error occurs. 359 - * 834 + * 360 835 * @param error - The error that occurred. 361 - * 836 + * 362 837 * Override this method in a subclass to implement custom error handling. 838 + * Note: Service switching is now handled in the reconnection logic, not here. 363 839 */ 364 - onError(error) { 840 + onError(_error) { 365 841 } 366 842 /** 367 843 * Called when the WebSocket connection is closed. 368 - * 844 + * 369 845 * Override this method in a subclass to implement custom logic on disconnection. 370 846 */ 371 847 onClose() { 372 848 } 373 849 /** 374 850 * Sends data to the connected WebSocket server, if the connection is open. 375 - * 851 + * 376 852 * @param data - The data to send. 377 853 */ 378 854 send(data) { ··· 384 860 * Closes the WebSocket connection gracefully. 385 861 */ 386 862 close() { 863 + this.shouldReconnect = false; 864 + this.stopHeartbeat(); 865 + if (this.reconnectTimeout) { 866 + clearTimeout(this.reconnectTimeout); 867 + this.reconnectTimeout = null; 868 + } 387 869 if (this.ws) { 388 870 this.ws.close(); 389 871 } 872 + healthMonitor.unregisterHealthCheck(this.healthCheckName); 873 + } 874 + getConnectionState() { 875 + if (!this.ws) return "DISCONNECTED"; 876 + switch (this.ws.readyState) { 877 + case import_ws.default.CONNECTING: 878 + return "CONNECTING"; 879 + case import_ws.default.OPEN: 880 + return "CONNECTED"; 881 + case import_ws.default.CLOSING: 882 + return "CLOSING"; 883 + case import_ws.default.CLOSED: 884 + return "DISCONNECTED"; 885 + default: 886 + return "UNKNOWN"; 887 + } 888 + } 889 + getReconnectAttempts() { 890 + return this.reconnectAttempts; 891 + } 892 + getServiceCycles() { 893 + return this.serviceCycles; 894 + } 895 + getServiceIndex() { 896 + return this.serviceIndex; 897 + } 898 + getAllServices() { 899 + return Array.isArray(this.service) ? [...this.service] : [this.service]; 900 + } 901 + getCurrentService() { 902 + return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service; 903 + } 904 + getMessageCount() { 905 + return this.messageCount; 906 + } 907 + getLastMessageTime() { 908 + return this.lastMessageTime; 909 + } 910 + getHealthCheckName() { 911 + return this.healthCheckName; 390 912 } 391 913 }; 392 914 ··· 394 916 var JetstreamSubscription = class extends WebSocketClient { 395 917 /** 396 918 * Creates a new `JetstreamSubscription`. 397 - * 398 - * @param service - The URL of the Jetstream server to connect to. 919 + * 920 + * @param service - The URL(-Array) of the Jetstream server(s) to connect to. 399 921 * @param interval - The interval (in milliseconds) for reconnect attempts. 400 922 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 401 923 */ 402 924 constructor(service, interval, onMessageCallback) { 403 - super({ url: service, reconnectInterval: interval }); 404 - this.service = service; 925 + super({ service, reconnectInterval: interval }); 405 926 this.interval = interval; 406 927 this.onMessageCallback = onMessageCallback; 407 928 } ··· 411 932 */ 412 933 onOpen() { 413 934 Logger.info("Connected to Jetstream server."); 935 + super.onOpen(); 414 936 } 415 937 /** 416 938 * Called when a WebSocket message is received. 417 - * 939 + * 418 940 * If an `onMessageCallback` was provided, it is invoked with the received data. 419 - * 941 + * 420 942 * @param data - The data received from the Jetstream server. 421 943 */ 422 944 onMessage(data) { ··· 427 949 /** 428 950 * Called when a WebSocket error occurs. 429 951 * Logs the error message indicating that Jetstream encountered an error. 430 - * 952 + * 431 953 * @param error - The error that occurred. 432 954 */ 433 955 onError(error) { 434 956 Logger.error("Jetstream encountered an error:", error); 957 + super.onError(error); 435 958 } 436 959 /** 437 960 * Called when the WebSocket connection is closed. ··· 439 962 */ 440 963 onClose() { 441 964 Logger.info("Jetstream connection closed."); 965 + super.onClose(); 442 966 } 443 967 }; 444 968 ··· 475 999 0 && (module.exports = { 476 1000 ActionBotAgent, 477 1001 CronBotAgent, 1002 + HealthMonitor, 478 1003 JetstreamSubscription, 479 1004 KeywordBotAgent, 1005 + LogLevel, 480 1006 Logger, 481 1007 WebSocketClient, 482 1008 buildReplyToPost, 483 1009 filterBotReplies, 1010 + healthMonitor, 484 1011 maybeInt, 485 1012 maybeStr, 486 1013 useActionBotAgent,
+1 -1
dist/index.js.map
··· 1 - {"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"]} 1 + {"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
··· 1 + var __defProp = Object.defineProperty; 2 + var __getOwnPropSymbols = Object.getOwnPropertySymbols; 3 + var __hasOwnProp = Object.prototype.hasOwnProperty; 4 + var __propIsEnum = Object.prototype.propertyIsEnumerable; 5 + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 6 + var __spreadValues = (a, b) => { 7 + for (var prop in b || (b = {})) 8 + if (__hasOwnProp.call(b, prop)) 9 + __defNormalProp(a, prop, b[prop]); 10 + if (__getOwnPropSymbols) 11 + for (var prop of __getOwnPropSymbols(b)) { 12 + if (__propIsEnum.call(b, prop)) 13 + __defNormalProp(a, prop, b[prop]); 14 + } 15 + return a; 16 + }; 1 17 var __async = (__this, __arguments, generator) => { 2 18 return new Promise((resolve, reject) => { 3 19 var fulfilled = (value) => { ··· 23 39 import { AtpAgent } from "@atproto/api"; 24 40 25 41 // src/utils/logger.ts 42 + var LogLevel = /* @__PURE__ */ ((LogLevel2) => { 43 + LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG"; 44 + LogLevel2[LogLevel2["INFO"] = 1] = "INFO"; 45 + LogLevel2[LogLevel2["WARN"] = 2] = "WARN"; 46 + LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR"; 47 + return LogLevel2; 48 + })(LogLevel || {}); 26 49 var Logger = class { 27 50 /** 51 + * Generate a new correlation ID for tracking related operations. 52 + */ 53 + static generateCorrelationId() { 54 + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 55 + } 56 + /** 57 + * Set the correlation ID for subsequent log entries. 58 + * @param id - The correlation ID to use, or null to generate a new one 59 + */ 60 + static setCorrelationId(id) { 61 + this.correlationId = id || this.generateCorrelationId(); 62 + } 63 + /** 64 + * Get the current correlation ID. 65 + */ 66 + static getCorrelationId() { 67 + return this.correlationId; 68 + } 69 + /** 70 + * Clear the current correlation ID. 71 + */ 72 + static clearCorrelationId() { 73 + this.correlationId = null; 74 + } 75 + /** 76 + * Set the minimum log level. Messages below this level will not be logged. 77 + * @param level - The minimum log level 78 + */ 79 + static setLogLevel(level) { 80 + this.logLevel = level; 81 + } 82 + /** 83 + * Set the timezone for log timestamps. 84 + * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC") 85 + */ 86 + static setTimezone(timezone) { 87 + this.timezone = timezone; 88 + } 89 + /** 90 + * Get the current log level. 91 + */ 92 + static getLogLevel() { 93 + return this.logLevel; 94 + } 95 + /** 96 + * Generate a formatted timestamp string. 97 + * @private 98 + */ 99 + static getTimestamp() { 100 + return (/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: this.timezone }); 101 + } 102 + /** 103 + * Internal logging method that checks log level before processing. 104 + * @private 105 + */ 106 + static log(level, levelName, message, context, logFn = console.log) { 107 + if (level < this.logLevel) { 108 + return; 109 + } 110 + const timestamp = this.getTimestamp(); 111 + let formattedMessage = `${timestamp} [${levelName}]`; 112 + if (this.correlationId) { 113 + formattedMessage += ` [${this.correlationId}]`; 114 + } 115 + if (context && typeof context === "object" && "correlationId" in context && context.correlationId && context.correlationId !== this.correlationId) { 116 + formattedMessage += ` [${context.correlationId}]`; 117 + } 118 + formattedMessage += `: ${message}`; 119 + if (context) { 120 + if (typeof context === "object") { 121 + const logEntry = __spreadValues({ 122 + timestamp: (/* @__PURE__ */ new Date()).toISOString(), 123 + level: levelName, 124 + message, 125 + correlationId: this.correlationId 126 + }, context); 127 + logFn(formattedMessage, logEntry); 128 + } else { 129 + logFn(formattedMessage, context); 130 + } 131 + } else { 132 + logFn(formattedMessage); 133 + } 134 + } 135 + /** 28 136 * Logs an informational message to the console. 29 137 * 30 138 * @param message - The message to be logged. 31 - * @param context - Optional additional context (object or string) to log alongside the message. 139 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 32 140 */ 33 141 static info(message, context) { 34 - console.info(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [INFO]: ${message}`, context || ""); 142 + this.log(1 /* INFO */, "INFO", message, context, console.info); 35 143 } 36 144 /** 37 145 * Logs a warning message to the console. 38 146 * 39 147 * @param message - The message to be logged. 40 - * @param context - Optional additional context (object or string) to log alongside the message. 148 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 41 149 */ 42 150 static warn(message, context) { 43 - console.warn(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [WARNING]: ${message}`, context || ""); 151 + this.log(2 /* WARN */, "WARNING", message, context, console.warn); 44 152 } 45 153 /** 46 154 * Logs an error message to the console. 47 155 * 48 156 * @param message - The message to be logged. 49 - * @param context - Optional additional context (object or string) to log alongside the message. 157 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 50 158 */ 51 159 static error(message, context) { 52 - console.error(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [ERROR]: ${message}`, context || ""); 160 + this.log(3 /* ERROR */, "ERROR", message, context, console.error); 53 161 } 54 162 /** 55 163 * Logs a debug message to the console. 56 164 * 57 165 * @param message - The message to be logged. 58 - * @param context - Optional additional context (object or string) to log alongside the message. 166 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 59 167 */ 60 168 static debug(message, context) { 61 - console.debug(`${(/* @__PURE__ */ new Date()).toLocaleString("de-DE", { timeZone: "Europe/Vienna" })} [DEBUG]: ${message}`, context || ""); 169 + this.log(0 /* DEBUG */, "DEBUG", message, context, console.debug); 170 + } 171 + /** 172 + * Log operation start with timing. 173 + * @param operation - The operation name 174 + * @param context - Additional context 175 + */ 176 + static startOperation(operation, context) { 177 + const correlationId = (context == null ? void 0 : context.correlationId) || this.generateCorrelationId(); 178 + this.setCorrelationId(correlationId); 179 + this.info(`Starting operation: ${operation}`, __spreadValues({ 180 + operation, 181 + correlationId 182 + }, context)); 183 + return correlationId; 184 + } 185 + /** 186 + * Log operation completion with timing. 187 + * @param operation - The operation name 188 + * @param startTime - The start time from Date.now() 189 + * @param context - Additional context 190 + */ 191 + static endOperation(operation, startTime, context) { 192 + const duration = Date.now() - startTime; 193 + this.info(`Completed operation: ${operation}`, __spreadValues({ 194 + operation, 195 + duration: `${duration}ms` 196 + }, context)); 62 197 } 63 198 }; 199 + Logger.logLevel = 1 /* INFO */; 200 + Logger.timezone = "Europe/Vienna"; 201 + Logger.correlationId = null; 64 202 65 203 // src/bots/actionBot.ts 66 204 var ActionBotAgent = class extends AtpAgent { ··· 71 209 } 72 210 doAction(params) { 73 211 return __async(this, null, function* () { 74 - this.actionBot.action(this, params); 212 + const correlationId = Logger.startOperation("actionBot.doAction", { 213 + botId: this.actionBot.username || this.actionBot.identifier 214 + }); 215 + const startTime = Date.now(); 216 + try { 217 + yield this.actionBot.action(this, params); 218 + Logger.endOperation("actionBot.doAction", startTime, { 219 + correlationId, 220 + botId: this.actionBot.username || this.actionBot.identifier 221 + }); 222 + } catch (error) { 223 + Logger.error("Action bot execution failed", { 224 + correlationId, 225 + botId: this.actionBot.username || this.actionBot.identifier, 226 + error: error instanceof Error ? error.message : String(error) 227 + }); 228 + throw error; 229 + } 75 230 }); 76 231 } 77 232 }; 78 233 var useActionBotAgent = (actionBot) => __async(void 0, null, function* () { 79 - var _a, _b, _c; 234 + var _a; 235 + const botId = (_a = actionBot.username) != null ? _a : actionBot.identifier; 236 + const correlationId = Logger.startOperation("initializeActionBot", { botId }); 237 + const startTime = Date.now(); 80 238 const agent = new ActionBotAgent({ service: actionBot.service }, actionBot); 81 239 try { 82 - Logger.info(`Initialize action bot ${(_a = actionBot.username) != null ? _a : actionBot.identifier}`); 83 - const login = yield agent.login({ identifier: actionBot.identifier, password: actionBot.password }); 240 + Logger.info("Initializing action bot", { correlationId, botId }); 241 + const login = yield agent.login({ 242 + identifier: actionBot.identifier, 243 + password: actionBot.password 244 + }); 84 245 if (!login.success) { 85 - Logger.warn(`Failed to login action bot ${(_b = actionBot.username) != null ? _b : actionBot.identifier}`); 246 + Logger.warn("Action bot login failed", { correlationId, botId }); 86 247 return null; 87 248 } 249 + Logger.endOperation("initializeActionBot", startTime, { correlationId, botId }); 88 250 return agent; 89 251 } catch (error) { 90 - Logger.error("Failed to initialize action bot:", `${error}, ${(_c = actionBot.username) != null ? _c : actionBot.identifier}`); 252 + Logger.error("Failed to initialize action bot", { 253 + correlationId, 254 + botId, 255 + error: error.message, 256 + duration: Date.now() - startTime 257 + }); 91 258 return null; 92 259 } 93 260 }); ··· 116 283 const agent = new CronBotAgent({ service: cronBot.service }, cronBot); 117 284 try { 118 285 Logger.info(`Initialize cron bot ${(_a = cronBot.username) != null ? _a : cronBot.identifier}`); 119 - const login = yield agent.login({ identifier: cronBot.identifier, password: cronBot.password }); 286 + const login = yield agent.login({ 287 + identifier: cronBot.identifier, 288 + password: cronBot.password 289 + }); 120 290 if (!login.success) { 121 291 Logger.info(`Failed to login cron bot ${(_b = cronBot.username) != null ? _b : cronBot.identifier}`); 122 292 return null; ··· 124 294 agent.job.start(); 125 295 return agent; 126 296 } catch (error) { 127 - Logger.error("Failed to initialize cron bot:", `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}`); 297 + Logger.error( 298 + "Failed to initialize cron bot:", 299 + `${error}, ${(_c = cronBot.username) != null ? _c : cronBot.identifier}` 300 + ); 128 301 return null; 129 302 } 130 303 }); ··· 161 334 message 162 335 ); 163 336 yield Promise.all([this.like(post.uri, post.cid), this.post(reply)]); 164 - Logger.info(`Replied to post: ${post.uri}`, (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier); 337 + Logger.info( 338 + `Replied to post: ${post.uri}`, 339 + (_b = this.keywordBot.username) != null ? _b : this.keywordBot.identifier 340 + ); 165 341 } 166 342 } catch (error) { 167 - Logger.error("Error while replying:", `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}`); 343 + Logger.error( 344 + "Error while replying:", 345 + `${error}, ${(_c = this.keywordBot.username) != null ? _c : this.keywordBot.identifier}` 346 + ); 168 347 } 169 348 }); 170 349 } ··· 174 353 $type: "app.bsky.feed.post", 175 354 text: message, 176 355 reply: { 177 - "root": root, 178 - "parent": parent 356 + root, 357 + parent 179 358 } 180 359 }; 181 360 } 182 361 function filterBotReplies(text, botReplies) { 362 + const lowerText = text.toLowerCase(); 183 363 return botReplies.filter((reply) => { 184 364 const keyword = reply.keyword.toLowerCase(); 185 - const keywordFound = text.toLowerCase().includes(keyword); 186 - if (!keywordFound) { 365 + if (!lowerText.includes(keyword)) { 187 366 return false; 188 367 } 189 - if (Array.isArray(reply.exclude) && reply.exclude.length > 0) { 190 - for (const excludeWord of reply.exclude) { 191 - if (text.toLowerCase().includes(excludeWord.toLowerCase())) { 192 - return false; 193 - } 194 - } 368 + if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) { 369 + return true; 195 370 } 196 - return true; 371 + const hasExcludedWord = reply.exclude.some( 372 + (excludeWord) => lowerText.includes(excludeWord.toLowerCase()) 373 + ); 374 + return !hasExcludedWord; 197 375 }); 198 376 } 199 377 var useKeywordBotAgent = (keywordBot) => __async(void 0, null, function* () { 200 378 var _a, _b, _c; 201 379 const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot); 202 380 try { 203 - const login = yield agent.login({ identifier: keywordBot.identifier, password: keywordBot.password }); 381 + const login = yield agent.login({ 382 + identifier: keywordBot.identifier, 383 + password: keywordBot.password 384 + }); 204 385 Logger.info(`Initialize keyword bot ${(_a = keywordBot.username) != null ? _a : keywordBot.identifier}`); 205 386 if (!login.success) { 206 387 Logger.warn(`Failed to login keyword bot ${(_b = keywordBot.username) != null ? _b : keywordBot.identifier}`); ··· 208 389 } 209 390 return agent; 210 391 } catch (error) { 211 - Logger.error("Failed to initialize keyword bot:", `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}`); 392 + Logger.error( 393 + "Failed to initialize keyword bot:", 394 + `${error}, ${(_c = keywordBot.username) != null ? _c : keywordBot.identifier}` 395 + ); 212 396 return null; 213 397 } 214 398 }); 215 399 216 400 // src/utils/websocketClient.ts 217 401 import WebSocket from "ws"; 402 + 403 + // src/utils/healthCheck.ts 404 + var HealthMonitor = class { 405 + constructor(options = {}) { 406 + this.checks = /* @__PURE__ */ new Map(); 407 + this.metrics = /* @__PURE__ */ new Map(); 408 + this.lastCheckResults = /* @__PURE__ */ new Map(); 409 + this.checkInterval = null; 410 + this.options = { 411 + interval: options.interval || 3e4, 412 + // 30 seconds 413 + timeout: options.timeout || 5e3, 414 + // 5 seconds 415 + retries: options.retries || 2 416 + }; 417 + } 418 + /** 419 + * Register a health check function. 420 + * @param name - Unique name for the health check 421 + * @param checkFn - Function that returns true if healthy 422 + */ 423 + registerHealthCheck(name, checkFn) { 424 + this.checks.set(name, checkFn); 425 + Logger.debug(`Registered health check: ${name}`); 426 + } 427 + /** 428 + * Remove a health check. 429 + * @param name - Name of the health check to remove 430 + */ 431 + unregisterHealthCheck(name) { 432 + this.checks.delete(name); 433 + this.lastCheckResults.delete(name); 434 + Logger.debug(`Unregistered health check: ${name}`); 435 + } 436 + /** 437 + * Set a metric value. 438 + * @param name - Metric name 439 + * @param value - Metric value 440 + */ 441 + setMetric(name, value) { 442 + this.metrics.set(name, value); 443 + } 444 + /** 445 + * Increment a counter metric. 446 + * @param name - Metric name 447 + * @param increment - Value to add (default: 1) 448 + */ 449 + incrementMetric(name, increment = 1) { 450 + const current = this.metrics.get(name) || 0; 451 + this.metrics.set(name, current + increment); 452 + } 453 + /** 454 + * Get current metric value. 455 + * @param name - Metric name 456 + * @returns Current value or 0 if not found 457 + */ 458 + getMetric(name) { 459 + return this.metrics.get(name) || 0; 460 + } 461 + /** 462 + * Get all current metrics. 463 + * @returns Object with all metrics 464 + */ 465 + getAllMetrics() { 466 + return Object.fromEntries(this.metrics); 467 + } 468 + /** 469 + * Run a single health check with timeout and retries. 470 + * @private 471 + */ 472 + runHealthCheck(name, checkFn) { 473 + return __async(this, null, function* () { 474 + for (let attempt = 0; attempt <= this.options.retries; attempt++) { 475 + try { 476 + const result = yield this.withTimeout(checkFn(), this.options.timeout); 477 + if (result) { 478 + return true; 479 + } 480 + } catch (error) { 481 + Logger.debug( 482 + `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`, 483 + { error: error.message } 484 + ); 485 + } 486 + } 487 + return false; 488 + }); 489 + } 490 + /** 491 + * Wrap a promise with a timeout. 492 + * @private 493 + */ 494 + withTimeout(promise, timeoutMs) { 495 + return Promise.race([ 496 + promise, 497 + new Promise( 498 + (_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) 499 + ) 500 + ]); 501 + } 502 + /** 503 + * Run all health checks and return the current health status. 504 + */ 505 + getHealthStatus() { 506 + return __async(this, null, function* () { 507 + const timestamp = Date.now(); 508 + const checkResults = {}; 509 + const details = {}; 510 + const checkPromises = Array.from(this.checks.entries()).map((_0) => __async(this, [_0], function* ([name, checkFn]) { 511 + const result = yield this.runHealthCheck(name, checkFn); 512 + checkResults[name] = result; 513 + this.lastCheckResults.set(name, result); 514 + if (!result) { 515 + details[`${name}_last_failure`] = (/* @__PURE__ */ new Date()).toISOString(); 516 + } 517 + return result; 518 + })); 519 + yield Promise.allSettled(checkPromises); 520 + const healthy = Object.values(checkResults).every((result) => result); 521 + const metrics = this.getAllMetrics(); 522 + return { 523 + healthy, 524 + timestamp, 525 + checks: checkResults, 526 + metrics, 527 + details 528 + }; 529 + }); 530 + } 531 + /** 532 + * Start periodic health monitoring. 533 + */ 534 + start() { 535 + if (this.checkInterval) { 536 + this.stop(); 537 + } 538 + Logger.info(`Starting health monitor with ${this.options.interval}ms interval`); 539 + this.checkInterval = setInterval(() => __async(this, null, function* () { 540 + try { 541 + const status = yield this.getHealthStatus(); 542 + if (!status.healthy) { 543 + const failedChecks = Object.entries(status.checks).filter(([, healthy]) => !healthy).map(([name]) => name); 544 + Logger.warn(`Health check failed`, { 545 + operation: "health_check", 546 + failed_checks: failedChecks, 547 + metrics: status.metrics 548 + }); 549 + } else { 550 + Logger.debug("Health check passed", { 551 + operation: "health_check", 552 + metrics: status.metrics 553 + }); 554 + } 555 + } catch (error) { 556 + Logger.error("Error during health check:", { error: error.message }); 557 + } 558 + }), this.options.interval); 559 + } 560 + /** 561 + * Stop periodic health monitoring. 562 + */ 563 + stop() { 564 + if (this.checkInterval) { 565 + clearInterval(this.checkInterval); 566 + this.checkInterval = null; 567 + Logger.info("Stopped health monitor"); 568 + } 569 + } 570 + /** 571 + * Get a summary of the last health check results. 572 + */ 573 + getLastCheckSummary() { 574 + return Object.fromEntries(this.lastCheckResults); 575 + } 576 + }; 577 + var healthMonitor = new HealthMonitor(); 578 + 579 + // src/utils/websocketClient.ts 218 580 var WebSocketClient = class { 219 581 /** 220 582 * Creates a new instance of `WebSocketClient`. 221 - * 583 + * 222 584 * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval. 223 585 */ 224 586 constructor(options) { 225 587 this.ws = null; 226 588 this.pingTimeout = null; 227 - this.url = options.url; 589 + this.serviceIndex = 0; 590 + this.reconnectAttempts = 0; 591 + this.serviceCycles = 0; 592 + this.reconnectTimeout = null; 593 + this.isConnecting = false; 594 + this.shouldReconnect = true; 595 + this.messageCount = 0; 596 + this.lastMessageTime = 0; 597 + this.service = options.service; 228 598 this.reconnectInterval = options.reconnectInterval || 5e3; 229 599 this.pingInterval = options.pingInterval || 1e4; 600 + this.maxReconnectAttempts = options.maxReconnectAttempts || 3; 601 + this.maxServiceCycles = options.maxServiceCycles || 2; 602 + this.maxReconnectDelay = options.maxReconnectDelay || 3e4; 603 + this.backoffFactor = options.backoffFactor || 1.5; 604 + this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; 605 + healthMonitor.registerHealthCheck(this.healthCheckName, () => __async(this, null, function* () { 606 + return this.getConnectionState() === "CONNECTED"; 607 + })); 608 + healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0); 609 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0); 230 610 this.run(); 231 611 } 232 612 /** 233 613 * Initiates a WebSocket connection to the specified URL. 234 - * 614 + * 235 615 * This method sets up event listeners for `open`, `message`, `error`, and `close` events. 236 616 * When the connection opens, it starts the heartbeat mechanism. 237 617 * On close, it attempts to reconnect after a specified interval. 238 618 */ 239 619 run() { 240 - this.ws = new WebSocket(this.url); 620 + if (this.isConnecting) { 621 + return; 622 + } 623 + this.isConnecting = true; 624 + const currentService = Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service; 625 + Logger.info(`Attempting to connect to WebSocket: ${currentService}`); 626 + this.ws = new WebSocket(currentService); 241 627 this.ws.on("open", () => { 242 - Logger.info("WebSocket connected"); 628 + Logger.info("WebSocket connected successfully", { 629 + service: this.getCurrentService(), 630 + serviceIndex: this.serviceIndex 631 + }); 632 + this.isConnecting = false; 633 + this.reconnectAttempts = 0; 634 + this.serviceCycles = 0; 635 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 243 636 this.startHeartbeat(); 244 637 this.onOpen(); 245 638 }); 246 639 this.ws.on("message", (data) => { 640 + this.messageCount++; 641 + this.lastMessageTime = Date.now(); 642 + healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`); 247 643 this.onMessage(data); 248 644 }); 249 645 this.ws.on("error", (error) => { 250 646 Logger.error("WebSocket error:", error); 647 + this.isConnecting = false; 251 648 this.onError(error); 252 649 }); 253 - this.ws.on("close", () => { 254 - Logger.info("WebSocket disconnected"); 650 + this.ws.on("close", (code, reason) => { 651 + Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`); 652 + this.isConnecting = false; 255 653 this.stopHeartbeat(); 256 654 this.onClose(); 257 - this.reconnect(); 655 + if (this.shouldReconnect) { 656 + this.scheduleReconnect(); 657 + } 258 658 }); 259 659 } 260 660 /** 261 661 * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 262 662 * It clears all event listeners on the old WebSocket and initiates a new connection. 263 663 */ 264 - reconnect() { 664 + scheduleReconnect() { 665 + this.reconnectAttempts++; 666 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 667 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { 668 + if (this.shouldTryNextService()) { 669 + this.moveToNextService(); 670 + return; 671 + } else { 672 + Logger.error("All services exhausted after maximum cycles", { 673 + totalServices: Array.isArray(this.service) ? this.service.length : 1, 674 + maxServiceCycles: this.maxServiceCycles, 675 + serviceCycles: this.serviceCycles 676 + }); 677 + return; 678 + } 679 + } 680 + const delay = Math.min( 681 + this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1), 682 + this.maxReconnectDelay 683 + ); 684 + Logger.info( 685 + `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`, 686 + { 687 + service: this.getCurrentService(), 688 + serviceIndex: this.serviceIndex, 689 + delay: `${delay}ms` 690 + } 691 + ); 692 + if (this.reconnectTimeout) { 693 + clearTimeout(this.reconnectTimeout); 694 + } 695 + this.reconnectTimeout = setTimeout(() => { 696 + this.cleanup(); 697 + this.run(); 698 + }, delay); 699 + } 700 + /** 701 + * Check if we should try the next service in the array. 702 + */ 703 + shouldTryNextService() { 704 + if (!Array.isArray(this.service)) { 705 + return false; 706 + } 707 + return this.serviceCycles < this.maxServiceCycles; 708 + } 709 + /** 710 + * Move to the next service in the array and reset reconnection attempts. 711 + */ 712 + moveToNextService() { 713 + if (!Array.isArray(this.service)) { 714 + return; 715 + } 716 + const previousIndex = this.serviceIndex; 717 + this.serviceIndex = (this.serviceIndex + 1) % this.service.length; 718 + if (this.serviceIndex === 0) { 719 + this.serviceCycles++; 720 + } 721 + this.reconnectAttempts = 0; 722 + Logger.info("Switching to next service", { 723 + previousService: this.service[previousIndex], 724 + previousIndex, 725 + newService: this.getCurrentService(), 726 + newIndex: this.serviceIndex, 727 + serviceCycle: this.serviceCycles 728 + }); 729 + this.cleanup(); 730 + this.run(); 731 + } 732 + cleanup() { 265 733 if (this.ws) { 266 734 this.ws.removeAllListeners(); 735 + if (this.ws.readyState === WebSocket.OPEN) { 736 + this.ws.close(); 737 + } 267 738 this.ws = null; 268 739 } 269 - setTimeout(() => this.run(), this.reconnectInterval); 740 + if (this.reconnectTimeout) { 741 + clearTimeout(this.reconnectTimeout); 742 + this.reconnectTimeout = null; 743 + } 270 744 } 271 745 /** 272 746 * Starts sending periodic ping messages to the server. 273 - * 747 + * 274 748 * This function uses `setInterval` to send a ping at the configured `pingInterval`. 275 749 * If the WebSocket is not open, pings are not sent. 276 750 */ ··· 292 766 } 293 767 /** 294 768 * Called when the WebSocket connection is successfully opened. 295 - * 769 + * 296 770 * Override this method in a subclass to implement custom logic on connection. 297 771 */ 298 772 onOpen() { 299 773 } 300 774 /** 301 775 * Called when a WebSocket message is received. 302 - * 776 + * 303 777 * @param data - The data received from the WebSocket server. 304 - * 778 + * 305 779 * Override this method in a subclass to implement custom message handling. 306 780 */ 307 - onMessage(data) { 781 + onMessage(_data) { 308 782 } 309 783 /** 310 784 * Called when a WebSocket error occurs. 311 - * 785 + * 312 786 * @param error - The error that occurred. 313 - * 787 + * 314 788 * Override this method in a subclass to implement custom error handling. 789 + * Note: Service switching is now handled in the reconnection logic, not here. 315 790 */ 316 - onError(error) { 791 + onError(_error) { 317 792 } 318 793 /** 319 794 * Called when the WebSocket connection is closed. 320 - * 795 + * 321 796 * Override this method in a subclass to implement custom logic on disconnection. 322 797 */ 323 798 onClose() { 324 799 } 325 800 /** 326 801 * Sends data to the connected WebSocket server, if the connection is open. 327 - * 802 + * 328 803 * @param data - The data to send. 329 804 */ 330 805 send(data) { ··· 336 811 * Closes the WebSocket connection gracefully. 337 812 */ 338 813 close() { 814 + this.shouldReconnect = false; 815 + this.stopHeartbeat(); 816 + if (this.reconnectTimeout) { 817 + clearTimeout(this.reconnectTimeout); 818 + this.reconnectTimeout = null; 819 + } 339 820 if (this.ws) { 340 821 this.ws.close(); 341 822 } 823 + healthMonitor.unregisterHealthCheck(this.healthCheckName); 824 + } 825 + getConnectionState() { 826 + if (!this.ws) return "DISCONNECTED"; 827 + switch (this.ws.readyState) { 828 + case WebSocket.CONNECTING: 829 + return "CONNECTING"; 830 + case WebSocket.OPEN: 831 + return "CONNECTED"; 832 + case WebSocket.CLOSING: 833 + return "CLOSING"; 834 + case WebSocket.CLOSED: 835 + return "DISCONNECTED"; 836 + default: 837 + return "UNKNOWN"; 838 + } 839 + } 840 + getReconnectAttempts() { 841 + return this.reconnectAttempts; 842 + } 843 + getServiceCycles() { 844 + return this.serviceCycles; 845 + } 846 + getServiceIndex() { 847 + return this.serviceIndex; 848 + } 849 + getAllServices() { 850 + return Array.isArray(this.service) ? [...this.service] : [this.service]; 851 + } 852 + getCurrentService() { 853 + return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service; 854 + } 855 + getMessageCount() { 856 + return this.messageCount; 857 + } 858 + getLastMessageTime() { 859 + return this.lastMessageTime; 860 + } 861 + getHealthCheckName() { 862 + return this.healthCheckName; 342 863 } 343 864 }; 344 865 ··· 346 867 var JetstreamSubscription = class extends WebSocketClient { 347 868 /** 348 869 * Creates a new `JetstreamSubscription`. 349 - * 350 - * @param service - The URL of the Jetstream server to connect to. 870 + * 871 + * @param service - The URL(-Array) of the Jetstream server(s) to connect to. 351 872 * @param interval - The interval (in milliseconds) for reconnect attempts. 352 873 * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 353 874 */ 354 875 constructor(service, interval, onMessageCallback) { 355 - super({ url: service, reconnectInterval: interval }); 356 - this.service = service; 876 + super({ service, reconnectInterval: interval }); 357 877 this.interval = interval; 358 878 this.onMessageCallback = onMessageCallback; 359 879 } ··· 363 883 */ 364 884 onOpen() { 365 885 Logger.info("Connected to Jetstream server."); 886 + super.onOpen(); 366 887 } 367 888 /** 368 889 * Called when a WebSocket message is received. 369 - * 890 + * 370 891 * If an `onMessageCallback` was provided, it is invoked with the received data. 371 - * 892 + * 372 893 * @param data - The data received from the Jetstream server. 373 894 */ 374 895 onMessage(data) { ··· 379 900 /** 380 901 * Called when a WebSocket error occurs. 381 902 * Logs the error message indicating that Jetstream encountered an error. 382 - * 903 + * 383 904 * @param error - The error that occurred. 384 905 */ 385 906 onError(error) { 386 907 Logger.error("Jetstream encountered an error:", error); 908 + super.onError(error); 387 909 } 388 910 /** 389 911 * Called when the WebSocket connection is closed. ··· 391 913 */ 392 914 onClose() { 393 915 Logger.info("Jetstream connection closed."); 916 + super.onClose(); 394 917 } 395 918 }; 396 919 ··· 426 949 export { 427 950 ActionBotAgent, 428 951 CronBotAgent, 952 + HealthMonitor, 429 953 JetstreamSubscription, 430 954 KeywordBotAgent, 955 + LogLevel, 431 956 Logger, 432 957 WebSocketClient, 433 958 buildReplyToPost, 434 959 filterBotReplies, 960 + healthMonitor, 435 961 maybeInt, 436 962 maybeStr, 437 963 useActionBotAgent,
+1 -1
dist/index.mjs.map
··· 1 - {"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"]} 1 + {"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"]}
+49
eslint.config.js
··· 1 + const js = require("@eslint/js"); 2 + const globals = require("globals"); 3 + const tseslint = require("@typescript-eslint/eslint-plugin"); 4 + const tsparser = require("@typescript-eslint/parser"); 5 + 6 + module.exports = [ 7 + js.configs.recommended, 8 + { 9 + files: ["src/**/*.ts"], 10 + languageOptions: { 11 + parser: tsparser, 12 + parserOptions: { 13 + ecmaVersion: 2022, 14 + sourceType: "module", 15 + project: "./tsconfig.json", 16 + }, 17 + globals: { 18 + ...globals.node, 19 + ...globals.es2022, 20 + NodeJS: "readonly", 21 + }, 22 + }, 23 + plugins: { 24 + "@typescript-eslint": tseslint, 25 + }, 26 + rules: { 27 + ...tseslint.configs.recommended.rules, 28 + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 29 + "@typescript-eslint/no-explicit-any": "warn", 30 + "@typescript-eslint/explicit-function-return-type": "off", 31 + "@typescript-eslint/no-inferrable-types": "off", 32 + "@typescript-eslint/ban-ts-comment": "warn", 33 + "prefer-const": "error", 34 + "no-var": "error", 35 + "no-console": "off", 36 + }, 37 + }, 38 + { 39 + files: ["**/*.js", "**/*.mjs"], 40 + languageOptions: { 41 + globals: { 42 + ...globals.node, 43 + }, 44 + }, 45 + }, 46 + { 47 + ignores: ["dist/**", "node_modules/**"], 48 + }, 49 + ];
+22 -2
package.json
··· 1 1 { 2 2 "name": "bskybot", 3 - "version": "1.2.11", 3 + "version": "2.0.0", 4 4 "description": "Create bluesky bots via configuration.", 5 5 "repository": "git@github.com:bskybot/bskybots.git", 6 6 "author": "bskybot <bot@eineseite.at>", ··· 18 18 "typescript" 19 19 ], 20 20 "scripts": { 21 - "build": "tsup" 21 + "build": "tsup", 22 + "lint": "eslint src --ext .ts", 23 + "lint:fix": "eslint src --ext .ts --fix", 24 + "format": "prettier --write \"src/**/*.ts\"", 25 + "format:check": "prettier --check \"src/**/*.ts\"", 26 + "typecheck": "tsc --noEmit", 27 + "prepare": "husky" 22 28 }, 23 29 "dependencies": { 24 30 "@atproto/api": "^0.14.16", ··· 28 34 }, 29 35 "devDependencies": { 30 36 "@types/node": "^22.13.14", 37 + "@typescript-eslint/eslint-plugin": "^8.18.0", 38 + "@typescript-eslint/parser": "^8.18.0", 39 + "eslint": "^9.16.0", 40 + "eslint-config-prettier": "^9.1.0", 41 + "eslint-plugin-prettier": "^5.2.1", 31 42 "globals": "^15.15.0", 43 + "husky": "^9.1.7", 44 + "lint-staged": "^15.3.0", 45 + "prettier": "^3.4.2", 32 46 "ts-node": "^10.9.2", 33 47 "tsup": "^8.4.0", 34 48 "typescript": "^5.8.2" 49 + }, 50 + "lint-staged": { 51 + "*.ts": [ 52 + "eslint --fix", 53 + "prettier --write" 54 + ] 35 55 } 36 56 }
+1243 -17
pnpm-lock.yaml
··· 4 4 autoInstallPeers: true 5 5 excludeLinksFromLockfile: false 6 6 7 - overrides: 8 - brace-expansion@>=2.0.0 <=2.0.1: '>=2.0.2' 9 - 10 7 importers: 11 8 12 9 .: ··· 27 24 '@types/node': 28 25 specifier: ^22.13.14 29 26 version: 22.13.14 27 + '@typescript-eslint/eslint-plugin': 28 + specifier: ^8.18.0 29 + version: 8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.2))(eslint@9.35.0)(typescript@5.8.2) 30 + '@typescript-eslint/parser': 31 + specifier: ^8.18.0 32 + version: 8.43.0(eslint@9.35.0)(typescript@5.8.2) 33 + eslint: 34 + specifier: ^9.16.0 35 + version: 9.35.0 36 + eslint-config-prettier: 37 + specifier: ^9.1.0 38 + version: 9.1.2(eslint@9.35.0) 39 + eslint-plugin-prettier: 40 + specifier: ^5.2.1 41 + version: 5.5.4(eslint-config-prettier@9.1.2(eslint@9.35.0))(eslint@9.35.0)(prettier@3.6.2) 30 42 globals: 31 43 specifier: ^15.15.0 32 44 version: 15.15.0 45 + husky: 46 + specifier: ^9.1.7 47 + version: 9.1.7 48 + lint-staged: 49 + specifier: ^15.3.0 50 + version: 15.5.2 51 + prettier: 52 + specifier: ^3.4.2 53 + version: 3.6.2 33 54 ts-node: 34 55 specifier: ^10.9.2 35 56 version: 10.9.2(@types/node@22.13.14)(typescript@5.8.2) 36 57 tsup: 37 58 specifier: ^8.4.0 38 - version: 8.4.0(typescript@5.8.2) 59 + version: 8.4.0(typescript@5.8.2)(yaml@2.8.1) 39 60 typescript: 40 61 specifier: ^5.8.2 41 62 version: 5.8.2 ··· 211 232 cpu: [x64] 212 233 os: [win32] 213 234 235 + '@eslint-community/eslint-utils@4.9.0': 236 + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} 237 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 238 + peerDependencies: 239 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 240 + 241 + '@eslint-community/regexpp@4.12.1': 242 + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 243 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 244 + 245 + '@eslint/config-array@0.21.0': 246 + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} 247 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 248 + 249 + '@eslint/config-helpers@0.3.1': 250 + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} 251 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 252 + 253 + '@eslint/core@0.15.2': 254 + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} 255 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 256 + 257 + '@eslint/eslintrc@3.3.1': 258 + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} 259 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 260 + 261 + '@eslint/js@9.35.0': 262 + resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} 263 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 264 + 265 + '@eslint/object-schema@2.1.6': 266 + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} 267 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 268 + 269 + '@eslint/plugin-kit@0.3.5': 270 + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} 271 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 272 + 273 + '@humanfs/core@0.19.1': 274 + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 275 + engines: {node: '>=18.18.0'} 276 + 277 + '@humanfs/node@0.16.7': 278 + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} 279 + engines: {node: '>=18.18.0'} 280 + 281 + '@humanwhocodes/module-importer@1.0.1': 282 + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 283 + engines: {node: '>=12.22'} 284 + 285 + '@humanwhocodes/retry@0.4.3': 286 + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 287 + engines: {node: '>=18.18'} 288 + 214 289 '@isaacs/cliui@8.0.2': 215 290 resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 216 291 engines: {node: '>=12'} ··· 236 311 '@jridgewell/trace-mapping@0.3.9': 237 312 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 238 313 314 + '@nodelib/fs.scandir@2.1.5': 315 + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 316 + engines: {node: '>= 8'} 317 + 318 + '@nodelib/fs.stat@2.0.5': 319 + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 320 + engines: {node: '>= 8'} 321 + 322 + '@nodelib/fs.walk@1.2.8': 323 + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 324 + engines: {node: '>= 8'} 325 + 239 326 '@pkgjs/parseargs@0.11.0': 240 327 resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 241 328 engines: {node: '>=14'} 329 + 330 + '@pkgr/core@0.2.9': 331 + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} 332 + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 242 333 243 334 '@rollup/rollup-android-arm-eabi@4.39.0': 244 335 resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} ··· 355 446 '@types/estree@1.0.7': 356 447 resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 357 448 449 + '@types/json-schema@7.0.15': 450 + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 451 + 358 452 '@types/luxon@3.4.2': 359 453 resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} 360 454 361 455 '@types/node@22.13.14': 362 456 resolution: {integrity: sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==} 363 457 458 + '@typescript-eslint/eslint-plugin@8.43.0': 459 + resolution: {integrity: sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==} 460 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 461 + peerDependencies: 462 + '@typescript-eslint/parser': ^8.43.0 463 + eslint: ^8.57.0 || ^9.0.0 464 + typescript: '>=4.8.4 <6.0.0' 465 + 466 + '@typescript-eslint/parser@8.43.0': 467 + resolution: {integrity: sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==} 468 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 469 + peerDependencies: 470 + eslint: ^8.57.0 || ^9.0.0 471 + typescript: '>=4.8.4 <6.0.0' 472 + 473 + '@typescript-eslint/project-service@8.43.0': 474 + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} 475 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 476 + peerDependencies: 477 + typescript: '>=4.8.4 <6.0.0' 478 + 479 + '@typescript-eslint/scope-manager@8.43.0': 480 + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} 481 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 482 + 483 + '@typescript-eslint/tsconfig-utils@8.43.0': 484 + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} 485 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 486 + peerDependencies: 487 + typescript: '>=4.8.4 <6.0.0' 488 + 489 + '@typescript-eslint/type-utils@8.43.0': 490 + resolution: {integrity: sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==} 491 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 492 + peerDependencies: 493 + eslint: ^8.57.0 || ^9.0.0 494 + typescript: '>=4.8.4 <6.0.0' 495 + 496 + '@typescript-eslint/types@8.43.0': 497 + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} 498 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 499 + 500 + '@typescript-eslint/typescript-estree@8.43.0': 501 + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} 502 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 503 + peerDependencies: 504 + typescript: '>=4.8.4 <6.0.0' 505 + 506 + '@typescript-eslint/utils@8.43.0': 507 + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} 508 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 509 + peerDependencies: 510 + eslint: ^8.57.0 || ^9.0.0 511 + typescript: '>=4.8.4 <6.0.0' 512 + 513 + '@typescript-eslint/visitor-keys@8.43.0': 514 + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} 515 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 516 + 517 + acorn-jsx@5.3.2: 518 + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 519 + peerDependencies: 520 + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 521 + 364 522 acorn-walk@8.3.4: 365 523 resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} 366 524 engines: {node: '>=0.4.0'} ··· 369 527 resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} 370 528 engines: {node: '>=0.4.0'} 371 529 hasBin: true 530 + 531 + acorn@8.15.0: 532 + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 533 + engines: {node: '>=0.4.0'} 534 + hasBin: true 535 + 536 + ajv@6.12.6: 537 + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 538 + 539 + ansi-escapes@7.0.0: 540 + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} 541 + engines: {node: '>=18'} 372 542 373 543 ansi-regex@5.0.1: 374 544 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} ··· 392 562 arg@4.1.3: 393 563 resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 394 564 565 + argparse@2.0.1: 566 + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 567 + 395 568 await-lock@2.2.2: 396 569 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 397 570 398 - balanced-match@3.0.1: 399 - resolution: {integrity: sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==} 400 - engines: {node: '>= 16'} 571 + balanced-match@1.0.2: 572 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 401 573 402 - brace-expansion@4.0.1: 403 - resolution: {integrity: sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==} 404 - engines: {node: '>= 18'} 574 + brace-expansion@1.1.12: 575 + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 576 + 577 + brace-expansion@2.0.2: 578 + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} 579 + 580 + braces@3.0.3: 581 + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 582 + engines: {node: '>=8'} 405 583 406 584 bundle-require@5.1.0: 407 585 resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} ··· 413 591 resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 414 592 engines: {node: '>=8'} 415 593 594 + callsites@3.1.0: 595 + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 596 + engines: {node: '>=6'} 597 + 598 + chalk@4.1.2: 599 + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 600 + engines: {node: '>=10'} 601 + 602 + chalk@5.6.2: 603 + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} 604 + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 605 + 416 606 chokidar@4.0.3: 417 607 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 418 608 engines: {node: '>= 14.16.0'} 609 + 610 + cli-cursor@5.0.0: 611 + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} 612 + engines: {node: '>=18'} 613 + 614 + cli-truncate@4.0.0: 615 + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} 616 + engines: {node: '>=18'} 419 617 420 618 color-convert@2.0.1: 421 619 resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} ··· 424 622 color-name@1.1.4: 425 623 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 426 624 625 + colorette@2.0.20: 626 + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} 627 + 628 + commander@13.1.0: 629 + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} 630 + engines: {node: '>=18'} 631 + 427 632 commander@4.1.1: 428 633 resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 429 634 engines: {node: '>= 6'} 635 + 636 + concat-map@0.0.1: 637 + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 430 638 431 639 consola@3.4.2: 432 640 resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} ··· 452 660 supports-color: 453 661 optional: true 454 662 663 + deep-is@0.1.4: 664 + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 665 + 455 666 diff@4.0.2: 456 667 resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 457 668 engines: {node: '>=0.3.1'} ··· 459 670 eastasianwidth@0.2.0: 460 671 resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 461 672 673 + emoji-regex@10.5.0: 674 + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} 675 + 462 676 emoji-regex@8.0.0: 463 677 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 464 678 465 679 emoji-regex@9.2.2: 466 680 resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 467 681 682 + environment@1.1.0: 683 + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} 684 + engines: {node: '>=18'} 685 + 468 686 esbuild@0.25.2: 469 687 resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} 470 688 engines: {node: '>=18'} 471 689 hasBin: true 472 690 691 + escape-string-regexp@4.0.0: 692 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 693 + engines: {node: '>=10'} 694 + 695 + eslint-config-prettier@9.1.2: 696 + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} 697 + hasBin: true 698 + peerDependencies: 699 + eslint: '>=7.0.0' 700 + 701 + eslint-plugin-prettier@5.5.4: 702 + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} 703 + engines: {node: ^14.18.0 || >=16.0.0} 704 + peerDependencies: 705 + '@types/eslint': '>=8.0.0' 706 + eslint: '>=8.0.0' 707 + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' 708 + prettier: '>=3.0.0' 709 + peerDependenciesMeta: 710 + '@types/eslint': 711 + optional: true 712 + eslint-config-prettier: 713 + optional: true 714 + 715 + eslint-scope@8.4.0: 716 + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} 717 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 718 + 719 + eslint-visitor-keys@3.4.3: 720 + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 721 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 722 + 723 + eslint-visitor-keys@4.2.1: 724 + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} 725 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 726 + 727 + eslint@9.35.0: 728 + resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} 729 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 730 + hasBin: true 731 + peerDependencies: 732 + jiti: '*' 733 + peerDependenciesMeta: 734 + jiti: 735 + optional: true 736 + 737 + espree@10.4.0: 738 + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 739 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 740 + 741 + esquery@1.6.0: 742 + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 743 + engines: {node: '>=0.10'} 744 + 745 + esrecurse@4.3.0: 746 + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 747 + engines: {node: '>=4.0'} 748 + 749 + estraverse@5.3.0: 750 + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 751 + engines: {node: '>=4.0'} 752 + 753 + esutils@2.0.3: 754 + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 755 + engines: {node: '>=0.10.0'} 756 + 757 + eventemitter3@5.0.1: 758 + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 759 + 760 + execa@8.0.1: 761 + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} 762 + engines: {node: '>=16.17'} 763 + 764 + fast-deep-equal@3.1.3: 765 + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 766 + 767 + fast-diff@1.3.0: 768 + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} 769 + 770 + fast-glob@3.3.3: 771 + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 772 + engines: {node: '>=8.6.0'} 773 + 774 + fast-json-stable-stringify@2.1.0: 775 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 776 + 777 + fast-levenshtein@2.0.6: 778 + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 779 + 780 + fastq@1.19.1: 781 + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 782 + 473 783 fdir@6.4.3: 474 784 resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} 475 785 peerDependencies: ··· 478 788 picomatch: 479 789 optional: true 480 790 791 + file-entry-cache@8.0.0: 792 + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 793 + engines: {node: '>=16.0.0'} 794 + 795 + fill-range@7.1.1: 796 + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 797 + engines: {node: '>=8'} 798 + 799 + find-up@5.0.0: 800 + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 801 + engines: {node: '>=10'} 802 + 803 + flat-cache@4.0.1: 804 + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 805 + engines: {node: '>=16'} 806 + 807 + flatted@3.3.3: 808 + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 809 + 481 810 foreground-child@3.3.1: 482 811 resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} 483 812 engines: {node: '>=14'} ··· 487 816 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 488 817 os: [darwin] 489 818 819 + get-east-asian-width@1.3.1: 820 + resolution: {integrity: sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==} 821 + engines: {node: '>=18'} 822 + 823 + get-stream@8.0.1: 824 + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} 825 + engines: {node: '>=16'} 826 + 827 + glob-parent@5.1.2: 828 + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 829 + engines: {node: '>= 6'} 830 + 831 + glob-parent@6.0.2: 832 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 833 + engines: {node: '>=10.13.0'} 834 + 490 835 glob@10.4.5: 491 836 resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 492 837 hasBin: true 493 838 839 + globals@14.0.0: 840 + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 841 + engines: {node: '>=18'} 842 + 494 843 globals@15.15.0: 495 844 resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} 496 845 engines: {node: '>=18'} ··· 498 847 graphemer@1.4.0: 499 848 resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 500 849 850 + has-flag@4.0.0: 851 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 852 + engines: {node: '>=8'} 853 + 854 + human-signals@5.0.0: 855 + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} 856 + engines: {node: '>=16.17.0'} 857 + 858 + husky@9.1.7: 859 + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} 860 + engines: {node: '>=18'} 861 + hasBin: true 862 + 863 + ignore@5.3.2: 864 + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 865 + engines: {node: '>= 4'} 866 + 867 + ignore@7.0.5: 868 + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} 869 + engines: {node: '>= 4'} 870 + 871 + import-fresh@3.3.1: 872 + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 873 + engines: {node: '>=6'} 874 + 875 + imurmurhash@0.1.4: 876 + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 877 + engines: {node: '>=0.8.19'} 878 + 879 + is-extglob@2.1.1: 880 + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 881 + engines: {node: '>=0.10.0'} 882 + 501 883 is-fullwidth-code-point@3.0.0: 502 884 resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 503 885 engines: {node: '>=8'} 504 886 887 + is-fullwidth-code-point@4.0.0: 888 + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} 889 + engines: {node: '>=12'} 890 + 891 + is-fullwidth-code-point@5.1.0: 892 + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} 893 + engines: {node: '>=18'} 894 + 895 + is-glob@4.0.3: 896 + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 897 + engines: {node: '>=0.10.0'} 898 + 899 + is-number@7.0.0: 900 + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 901 + engines: {node: '>=0.12.0'} 902 + 903 + is-stream@3.0.0: 904 + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 905 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 906 + 505 907 isexe@2.0.0: 506 908 resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 507 909 ··· 515 917 resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 516 918 engines: {node: '>=10'} 517 919 920 + js-yaml@4.1.0: 921 + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 922 + hasBin: true 923 + 924 + json-buffer@3.0.1: 925 + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 926 + 927 + json-schema-traverse@0.4.1: 928 + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 929 + 930 + json-stable-stringify-without-jsonify@1.0.1: 931 + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 932 + 933 + keyv@4.5.4: 934 + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 935 + 936 + levn@0.4.1: 937 + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 938 + engines: {node: '>= 0.8.0'} 939 + 518 940 lilconfig@3.1.3: 519 941 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 520 942 engines: {node: '>=14'} ··· 522 944 lines-and-columns@1.2.4: 523 945 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 524 946 947 + lint-staged@15.5.2: 948 + resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} 949 + engines: {node: '>=18.12.0'} 950 + hasBin: true 951 + 952 + listr2@8.3.3: 953 + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} 954 + engines: {node: '>=18.0.0'} 955 + 525 956 load-tsconfig@0.2.5: 526 957 resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 527 958 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 528 959 960 + locate-path@6.0.0: 961 + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 962 + engines: {node: '>=10'} 963 + 964 + lodash.merge@4.6.2: 965 + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 966 + 529 967 lodash.sortby@4.7.0: 530 968 resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} 531 969 970 + log-update@6.1.0: 971 + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} 972 + engines: {node: '>=18'} 973 + 532 974 lru-cache@10.4.3: 533 975 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 534 976 ··· 539 981 make-error@1.3.6: 540 982 resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 541 983 984 + merge-stream@2.0.0: 985 + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 986 + 987 + merge2@1.4.1: 988 + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 989 + engines: {node: '>= 8'} 990 + 991 + micromatch@4.0.8: 992 + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 993 + engines: {node: '>=8.6'} 994 + 995 + mimic-fn@4.0.0: 996 + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} 997 + engines: {node: '>=12'} 998 + 999 + mimic-function@5.0.1: 1000 + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} 1001 + engines: {node: '>=18'} 1002 + 1003 + minimatch@3.1.2: 1004 + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1005 + 542 1006 minimatch@9.0.5: 543 1007 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 544 1008 engines: {node: '>=16 || 14 >=14.17'} ··· 556 1020 mz@2.7.0: 557 1021 resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 558 1022 1023 + natural-compare@1.4.0: 1024 + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1025 + 1026 + npm-run-path@5.3.0: 1027 + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} 1028 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1029 + 559 1030 object-assign@4.1.1: 560 1031 resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 561 1032 engines: {node: '>=0.10.0'} 562 1033 1034 + onetime@6.0.0: 1035 + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} 1036 + engines: {node: '>=12'} 1037 + 1038 + onetime@7.0.0: 1039 + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} 1040 + engines: {node: '>=18'} 1041 + 1042 + optionator@0.9.4: 1043 + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1044 + engines: {node: '>= 0.8.0'} 1045 + 1046 + p-limit@3.1.0: 1047 + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1048 + engines: {node: '>=10'} 1049 + 1050 + p-locate@5.0.0: 1051 + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1052 + engines: {node: '>=10'} 1053 + 563 1054 package-json-from-dist@1.0.1: 564 1055 resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 565 1056 1057 + parent-module@1.0.1: 1058 + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1059 + engines: {node: '>=6'} 1060 + 1061 + path-exists@4.0.0: 1062 + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1063 + engines: {node: '>=8'} 1064 + 566 1065 path-key@3.1.1: 567 1066 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 568 1067 engines: {node: '>=8'} 1068 + 1069 + path-key@4.0.0: 1070 + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} 1071 + engines: {node: '>=12'} 569 1072 570 1073 path-scurry@1.11.1: 571 1074 resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} ··· 574 1077 picocolors@1.1.1: 575 1078 resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 576 1079 1080 + picomatch@2.3.1: 1081 + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1082 + engines: {node: '>=8.6'} 1083 + 577 1084 picomatch@4.0.2: 578 1085 resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} 579 1086 engines: {node: '>=12'} 1087 + 1088 + pidtree@0.6.0: 1089 + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} 1090 + engines: {node: '>=0.10'} 1091 + hasBin: true 580 1092 581 1093 pirates@4.0.7: 582 1094 resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} ··· 600 1112 yaml: 601 1113 optional: true 602 1114 1115 + prelude-ls@1.2.1: 1116 + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1117 + engines: {node: '>= 0.8.0'} 1118 + 1119 + prettier-linter-helpers@1.0.0: 1120 + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} 1121 + engines: {node: '>=6.0.0'} 1122 + 1123 + prettier@3.6.2: 1124 + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} 1125 + engines: {node: '>=14'} 1126 + hasBin: true 1127 + 603 1128 punycode@2.3.1: 604 1129 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 605 1130 engines: {node: '>=6'} 1131 + 1132 + queue-microtask@1.2.3: 1133 + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 606 1134 607 1135 readdirp@4.1.2: 608 1136 resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 609 1137 engines: {node: '>= 14.18.0'} 1138 + 1139 + resolve-from@4.0.0: 1140 + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1141 + engines: {node: '>=4'} 610 1142 611 1143 resolve-from@5.0.0: 612 1144 resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 613 1145 engines: {node: '>=8'} 614 1146 1147 + restore-cursor@5.1.0: 1148 + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} 1149 + engines: {node: '>=18'} 1150 + 1151 + reusify@1.1.0: 1152 + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1153 + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1154 + 1155 + rfdc@1.4.1: 1156 + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} 1157 + 615 1158 rollup@4.39.0: 616 1159 resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==} 617 1160 engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1161 + hasBin: true 1162 + 1163 + run-parallel@1.2.0: 1164 + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1165 + 1166 + semver@7.7.2: 1167 + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 1168 + engines: {node: '>=10'} 618 1169 hasBin: true 619 1170 620 1171 shebang-command@2.0.0: ··· 629 1180 resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 630 1181 engines: {node: '>=14'} 631 1182 1183 + slice-ansi@5.0.0: 1184 + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} 1185 + engines: {node: '>=12'} 1186 + 1187 + slice-ansi@7.1.2: 1188 + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} 1189 + engines: {node: '>=18'} 1190 + 632 1191 source-map@0.8.0-beta.0: 633 1192 resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} 634 1193 engines: {node: '>= 8'} 635 1194 1195 + string-argv@0.3.2: 1196 + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} 1197 + engines: {node: '>=0.6.19'} 1198 + 636 1199 string-width@4.2.3: 637 1200 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 638 1201 engines: {node: '>=8'} ··· 640 1203 string-width@5.1.2: 641 1204 resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 642 1205 engines: {node: '>=12'} 1206 + 1207 + string-width@7.2.0: 1208 + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} 1209 + engines: {node: '>=18'} 643 1210 644 1211 strip-ansi@6.0.1: 645 1212 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} ··· 649 1216 resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 650 1217 engines: {node: '>=12'} 651 1218 1219 + strip-final-newline@3.0.0: 1220 + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 1221 + engines: {node: '>=12'} 1222 + 1223 + strip-json-comments@3.1.1: 1224 + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1225 + engines: {node: '>=8'} 1226 + 652 1227 sucrase@3.35.0: 653 1228 resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 654 1229 engines: {node: '>=16 || 14 >=14.17'} 655 1230 hasBin: true 656 1231 1232 + supports-color@7.2.0: 1233 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1234 + engines: {node: '>=8'} 1235 + 1236 + synckit@0.11.11: 1237 + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} 1238 + engines: {node: ^14.18.0 || >=16.0.0} 1239 + 657 1240 thenify-all@1.6.0: 658 1241 resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 659 1242 engines: {node: '>=0.8'} ··· 672 1255 resolution: {integrity: sha512-ZmyVB9DAw+FFTmLElGYJgdZFsKLYd/I59Bg9NHkCGPwAbVZNRilFWDMAdX8UG+bHuv7kfursd5XGqo/9wi26lA==} 673 1256 hasBin: true 674 1257 1258 + to-regex-range@5.0.1: 1259 + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1260 + engines: {node: '>=8.0'} 1261 + 675 1262 tr46@1.0.1: 676 1263 resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} 677 1264 ··· 679 1266 resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 680 1267 hasBin: true 681 1268 1269 + ts-api-utils@2.1.0: 1270 + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} 1271 + engines: {node: '>=18.12'} 1272 + peerDependencies: 1273 + typescript: '>=4.8.4' 1274 + 682 1275 ts-interface-checker@0.1.13: 683 1276 resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 684 1277 ··· 715 1308 typescript: 716 1309 optional: true 717 1310 1311 + type-check@0.4.0: 1312 + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1313 + engines: {node: '>= 0.8.0'} 1314 + 718 1315 typescript@5.8.2: 719 1316 resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} 720 1317 engines: {node: '>=14.17'} ··· 726 1323 undici-types@6.20.0: 727 1324 resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 728 1325 1326 + uri-js@4.4.1: 1327 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1328 + 729 1329 v8-compile-cache-lib@3.0.1: 730 1330 resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 731 1331 ··· 740 1340 engines: {node: '>= 8'} 741 1341 hasBin: true 742 1342 1343 + word-wrap@1.2.5: 1344 + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1345 + engines: {node: '>=0.10.0'} 1346 + 743 1347 wrap-ansi@7.0.0: 744 1348 resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 745 1349 engines: {node: '>=10'} ··· 748 1352 resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 749 1353 engines: {node: '>=12'} 750 1354 1355 + wrap-ansi@9.0.2: 1356 + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} 1357 + engines: {node: '>=18'} 1358 + 751 1359 ws@8.18.1: 752 1360 resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} 753 1361 engines: {node: '>=10.0.0'} ··· 760 1368 utf-8-validate: 761 1369 optional: true 762 1370 1371 + yaml@2.8.1: 1372 + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} 1373 + engines: {node: '>= 14.6'} 1374 + hasBin: true 1375 + 763 1376 yn@3.1.1: 764 1377 resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 765 1378 engines: {node: '>=6'} 1379 + 1380 + yocto-queue@0.1.0: 1381 + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1382 + engines: {node: '>=10'} 766 1383 767 1384 zod@3.24.2: 768 1385 resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} ··· 881 1498 '@esbuild/win32-x64@0.25.2': 882 1499 optional: true 883 1500 1501 + '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)': 1502 + dependencies: 1503 + eslint: 9.35.0 1504 + eslint-visitor-keys: 3.4.3 1505 + 1506 + '@eslint-community/regexpp@4.12.1': {} 1507 + 1508 + '@eslint/config-array@0.21.0': 1509 + dependencies: 1510 + '@eslint/object-schema': 2.1.6 1511 + debug: 4.4.0 1512 + minimatch: 3.1.2 1513 + transitivePeerDependencies: 1514 + - supports-color 1515 + 1516 + '@eslint/config-helpers@0.3.1': {} 1517 + 1518 + '@eslint/core@0.15.2': 1519 + dependencies: 1520 + '@types/json-schema': 7.0.15 1521 + 1522 + '@eslint/eslintrc@3.3.1': 1523 + dependencies: 1524 + ajv: 6.12.6 1525 + debug: 4.4.0 1526 + espree: 10.4.0 1527 + globals: 14.0.0 1528 + ignore: 5.3.2 1529 + import-fresh: 3.3.1 1530 + js-yaml: 4.1.0 1531 + minimatch: 3.1.2 1532 + strip-json-comments: 3.1.1 1533 + transitivePeerDependencies: 1534 + - supports-color 1535 + 1536 + '@eslint/js@9.35.0': {} 1537 + 1538 + '@eslint/object-schema@2.1.6': {} 1539 + 1540 + '@eslint/plugin-kit@0.3.5': 1541 + dependencies: 1542 + '@eslint/core': 0.15.2 1543 + levn: 0.4.1 1544 + 1545 + '@humanfs/core@0.19.1': {} 1546 + 1547 + '@humanfs/node@0.16.7': 1548 + dependencies: 1549 + '@humanfs/core': 0.19.1 1550 + '@humanwhocodes/retry': 0.4.3 1551 + 1552 + '@humanwhocodes/module-importer@1.0.1': {} 1553 + 1554 + '@humanwhocodes/retry@0.4.3': {} 1555 + 884 1556 '@isaacs/cliui@8.0.2': 885 1557 dependencies: 886 1558 string-width: 5.1.2 ··· 912 1584 '@jridgewell/resolve-uri': 3.1.2 913 1585 '@jridgewell/sourcemap-codec': 1.5.0 914 1586 1587 + '@nodelib/fs.scandir@2.1.5': 1588 + dependencies: 1589 + '@nodelib/fs.stat': 2.0.5 1590 + run-parallel: 1.2.0 1591 + 1592 + '@nodelib/fs.stat@2.0.5': {} 1593 + 1594 + '@nodelib/fs.walk@1.2.8': 1595 + dependencies: 1596 + '@nodelib/fs.scandir': 2.1.5 1597 + fastq: 1.19.1 1598 + 915 1599 '@pkgjs/parseargs@0.11.0': 916 1600 optional: true 1601 + 1602 + '@pkgr/core@0.2.9': {} 917 1603 918 1604 '@rollup/rollup-android-arm-eabi@4.39.0': 919 1605 optional: true ··· 985 1671 986 1672 '@types/estree@1.0.7': {} 987 1673 1674 + '@types/json-schema@7.0.15': {} 1675 + 988 1676 '@types/luxon@3.4.2': {} 989 1677 990 1678 '@types/node@22.13.14': 991 1679 dependencies: 992 1680 undici-types: 6.20.0 993 1681 1682 + '@typescript-eslint/eslint-plugin@8.43.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.2))(eslint@9.35.0)(typescript@5.8.2)': 1683 + dependencies: 1684 + '@eslint-community/regexpp': 4.12.1 1685 + '@typescript-eslint/parser': 8.43.0(eslint@9.35.0)(typescript@5.8.2) 1686 + '@typescript-eslint/scope-manager': 8.43.0 1687 + '@typescript-eslint/type-utils': 8.43.0(eslint@9.35.0)(typescript@5.8.2) 1688 + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.2) 1689 + '@typescript-eslint/visitor-keys': 8.43.0 1690 + eslint: 9.35.0 1691 + graphemer: 1.4.0 1692 + ignore: 7.0.5 1693 + natural-compare: 1.4.0 1694 + ts-api-utils: 2.1.0(typescript@5.8.2) 1695 + typescript: 5.8.2 1696 + transitivePeerDependencies: 1697 + - supports-color 1698 + 1699 + '@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.8.2)': 1700 + dependencies: 1701 + '@typescript-eslint/scope-manager': 8.43.0 1702 + '@typescript-eslint/types': 8.43.0 1703 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.2) 1704 + '@typescript-eslint/visitor-keys': 8.43.0 1705 + debug: 4.4.0 1706 + eslint: 9.35.0 1707 + typescript: 5.8.2 1708 + transitivePeerDependencies: 1709 + - supports-color 1710 + 1711 + '@typescript-eslint/project-service@8.43.0(typescript@5.8.2)': 1712 + dependencies: 1713 + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.2) 1714 + '@typescript-eslint/types': 8.43.0 1715 + debug: 4.4.0 1716 + typescript: 5.8.2 1717 + transitivePeerDependencies: 1718 + - supports-color 1719 + 1720 + '@typescript-eslint/scope-manager@8.43.0': 1721 + dependencies: 1722 + '@typescript-eslint/types': 8.43.0 1723 + '@typescript-eslint/visitor-keys': 8.43.0 1724 + 1725 + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.8.2)': 1726 + dependencies: 1727 + typescript: 5.8.2 1728 + 1729 + '@typescript-eslint/type-utils@8.43.0(eslint@9.35.0)(typescript@5.8.2)': 1730 + dependencies: 1731 + '@typescript-eslint/types': 8.43.0 1732 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.2) 1733 + '@typescript-eslint/utils': 8.43.0(eslint@9.35.0)(typescript@5.8.2) 1734 + debug: 4.4.0 1735 + eslint: 9.35.0 1736 + ts-api-utils: 2.1.0(typescript@5.8.2) 1737 + typescript: 5.8.2 1738 + transitivePeerDependencies: 1739 + - supports-color 1740 + 1741 + '@typescript-eslint/types@8.43.0': {} 1742 + 1743 + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.8.2)': 1744 + dependencies: 1745 + '@typescript-eslint/project-service': 8.43.0(typescript@5.8.2) 1746 + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.8.2) 1747 + '@typescript-eslint/types': 8.43.0 1748 + '@typescript-eslint/visitor-keys': 8.43.0 1749 + debug: 4.4.0 1750 + fast-glob: 3.3.3 1751 + is-glob: 4.0.3 1752 + minimatch: 9.0.5 1753 + semver: 7.7.2 1754 + ts-api-utils: 2.1.0(typescript@5.8.2) 1755 + typescript: 5.8.2 1756 + transitivePeerDependencies: 1757 + - supports-color 1758 + 1759 + '@typescript-eslint/utils@8.43.0(eslint@9.35.0)(typescript@5.8.2)': 1760 + dependencies: 1761 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) 1762 + '@typescript-eslint/scope-manager': 8.43.0 1763 + '@typescript-eslint/types': 8.43.0 1764 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.8.2) 1765 + eslint: 9.35.0 1766 + typescript: 5.8.2 1767 + transitivePeerDependencies: 1768 + - supports-color 1769 + 1770 + '@typescript-eslint/visitor-keys@8.43.0': 1771 + dependencies: 1772 + '@typescript-eslint/types': 8.43.0 1773 + eslint-visitor-keys: 4.2.1 1774 + 1775 + acorn-jsx@5.3.2(acorn@8.15.0): 1776 + dependencies: 1777 + acorn: 8.15.0 1778 + 994 1779 acorn-walk@8.3.4: 995 1780 dependencies: 996 1781 acorn: 8.14.1 997 1782 998 1783 acorn@8.14.1: {} 999 1784 1785 + acorn@8.15.0: {} 1786 + 1787 + ajv@6.12.6: 1788 + dependencies: 1789 + fast-deep-equal: 3.1.3 1790 + fast-json-stable-stringify: 2.1.0 1791 + json-schema-traverse: 0.4.1 1792 + uri-js: 4.4.1 1793 + 1794 + ansi-escapes@7.0.0: 1795 + dependencies: 1796 + environment: 1.1.0 1797 + 1000 1798 ansi-regex@5.0.1: {} 1001 1799 1002 1800 ansi-regex@6.1.0: {} ··· 1011 1809 1012 1810 arg@4.1.3: {} 1013 1811 1812 + argparse@2.0.1: {} 1813 + 1014 1814 await-lock@2.2.2: {} 1015 1815 1016 - balanced-match@3.0.1: {} 1816 + balanced-match@1.0.2: {} 1817 + 1818 + brace-expansion@1.1.12: 1819 + dependencies: 1820 + balanced-match: 1.0.2 1821 + concat-map: 0.0.1 1822 + 1823 + brace-expansion@2.0.2: 1824 + dependencies: 1825 + balanced-match: 1.0.2 1017 1826 1018 - brace-expansion@4.0.1: 1827 + braces@3.0.3: 1019 1828 dependencies: 1020 - balanced-match: 3.0.1 1829 + fill-range: 7.1.1 1021 1830 1022 1831 bundle-require@5.1.0(esbuild@0.25.2): 1023 1832 dependencies: ··· 1026 1835 1027 1836 cac@6.7.14: {} 1028 1837 1838 + callsites@3.1.0: {} 1839 + 1840 + chalk@4.1.2: 1841 + dependencies: 1842 + ansi-styles: 4.3.0 1843 + supports-color: 7.2.0 1844 + 1845 + chalk@5.6.2: {} 1846 + 1029 1847 chokidar@4.0.3: 1030 1848 dependencies: 1031 1849 readdirp: 4.1.2 1032 1850 1851 + cli-cursor@5.0.0: 1852 + dependencies: 1853 + restore-cursor: 5.1.0 1854 + 1855 + cli-truncate@4.0.0: 1856 + dependencies: 1857 + slice-ansi: 5.0.0 1858 + string-width: 7.2.0 1859 + 1033 1860 color-convert@2.0.1: 1034 1861 dependencies: 1035 1862 color-name: 1.1.4 1036 1863 1037 1864 color-name@1.1.4: {} 1038 1865 1866 + colorette@2.0.20: {} 1867 + 1868 + commander@13.1.0: {} 1869 + 1039 1870 commander@4.1.1: {} 1040 1871 1872 + concat-map@0.0.1: {} 1873 + 1041 1874 consola@3.4.2: {} 1042 1875 1043 1876 create-require@1.1.1: {} ··· 1057 1890 dependencies: 1058 1891 ms: 2.1.3 1059 1892 1893 + deep-is@0.1.4: {} 1894 + 1060 1895 diff@4.0.2: {} 1061 1896 1062 1897 eastasianwidth@0.2.0: {} 1898 + 1899 + emoji-regex@10.5.0: {} 1063 1900 1064 1901 emoji-regex@8.0.0: {} 1065 1902 1066 1903 emoji-regex@9.2.2: {} 1067 1904 1905 + environment@1.1.0: {} 1906 + 1068 1907 esbuild@0.25.2: 1069 1908 optionalDependencies: 1070 1909 '@esbuild/aix-ppc64': 0.25.2 ··· 1093 1932 '@esbuild/win32-ia32': 0.25.2 1094 1933 '@esbuild/win32-x64': 0.25.2 1095 1934 1935 + escape-string-regexp@4.0.0: {} 1936 + 1937 + eslint-config-prettier@9.1.2(eslint@9.35.0): 1938 + dependencies: 1939 + eslint: 9.35.0 1940 + 1941 + eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@9.35.0))(eslint@9.35.0)(prettier@3.6.2): 1942 + dependencies: 1943 + eslint: 9.35.0 1944 + prettier: 3.6.2 1945 + prettier-linter-helpers: 1.0.0 1946 + synckit: 0.11.11 1947 + optionalDependencies: 1948 + eslint-config-prettier: 9.1.2(eslint@9.35.0) 1949 + 1950 + eslint-scope@8.4.0: 1951 + dependencies: 1952 + esrecurse: 4.3.0 1953 + estraverse: 5.3.0 1954 + 1955 + eslint-visitor-keys@3.4.3: {} 1956 + 1957 + eslint-visitor-keys@4.2.1: {} 1958 + 1959 + eslint@9.35.0: 1960 + dependencies: 1961 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) 1962 + '@eslint-community/regexpp': 4.12.1 1963 + '@eslint/config-array': 0.21.0 1964 + '@eslint/config-helpers': 0.3.1 1965 + '@eslint/core': 0.15.2 1966 + '@eslint/eslintrc': 3.3.1 1967 + '@eslint/js': 9.35.0 1968 + '@eslint/plugin-kit': 0.3.5 1969 + '@humanfs/node': 0.16.7 1970 + '@humanwhocodes/module-importer': 1.0.1 1971 + '@humanwhocodes/retry': 0.4.3 1972 + '@types/estree': 1.0.7 1973 + '@types/json-schema': 7.0.15 1974 + ajv: 6.12.6 1975 + chalk: 4.1.2 1976 + cross-spawn: 7.0.6 1977 + debug: 4.4.0 1978 + escape-string-regexp: 4.0.0 1979 + eslint-scope: 8.4.0 1980 + eslint-visitor-keys: 4.2.1 1981 + espree: 10.4.0 1982 + esquery: 1.6.0 1983 + esutils: 2.0.3 1984 + fast-deep-equal: 3.1.3 1985 + file-entry-cache: 8.0.0 1986 + find-up: 5.0.0 1987 + glob-parent: 6.0.2 1988 + ignore: 5.3.2 1989 + imurmurhash: 0.1.4 1990 + is-glob: 4.0.3 1991 + json-stable-stringify-without-jsonify: 1.0.1 1992 + lodash.merge: 4.6.2 1993 + minimatch: 3.1.2 1994 + natural-compare: 1.4.0 1995 + optionator: 0.9.4 1996 + transitivePeerDependencies: 1997 + - supports-color 1998 + 1999 + espree@10.4.0: 2000 + dependencies: 2001 + acorn: 8.15.0 2002 + acorn-jsx: 5.3.2(acorn@8.15.0) 2003 + eslint-visitor-keys: 4.2.1 2004 + 2005 + esquery@1.6.0: 2006 + dependencies: 2007 + estraverse: 5.3.0 2008 + 2009 + esrecurse@4.3.0: 2010 + dependencies: 2011 + estraverse: 5.3.0 2012 + 2013 + estraverse@5.3.0: {} 2014 + 2015 + esutils@2.0.3: {} 2016 + 2017 + eventemitter3@5.0.1: {} 2018 + 2019 + execa@8.0.1: 2020 + dependencies: 2021 + cross-spawn: 7.0.6 2022 + get-stream: 8.0.1 2023 + human-signals: 5.0.0 2024 + is-stream: 3.0.0 2025 + merge-stream: 2.0.0 2026 + npm-run-path: 5.3.0 2027 + onetime: 6.0.0 2028 + signal-exit: 4.1.0 2029 + strip-final-newline: 3.0.0 2030 + 2031 + fast-deep-equal@3.1.3: {} 2032 + 2033 + fast-diff@1.3.0: {} 2034 + 2035 + fast-glob@3.3.3: 2036 + dependencies: 2037 + '@nodelib/fs.stat': 2.0.5 2038 + '@nodelib/fs.walk': 1.2.8 2039 + glob-parent: 5.1.2 2040 + merge2: 1.4.1 2041 + micromatch: 4.0.8 2042 + 2043 + fast-json-stable-stringify@2.1.0: {} 2044 + 2045 + fast-levenshtein@2.0.6: {} 2046 + 2047 + fastq@1.19.1: 2048 + dependencies: 2049 + reusify: 1.1.0 2050 + 1096 2051 fdir@6.4.3(picomatch@4.0.2): 1097 2052 optionalDependencies: 1098 2053 picomatch: 4.0.2 1099 2054 2055 + file-entry-cache@8.0.0: 2056 + dependencies: 2057 + flat-cache: 4.0.1 2058 + 2059 + fill-range@7.1.1: 2060 + dependencies: 2061 + to-regex-range: 5.0.1 2062 + 2063 + find-up@5.0.0: 2064 + dependencies: 2065 + locate-path: 6.0.0 2066 + path-exists: 4.0.0 2067 + 2068 + flat-cache@4.0.1: 2069 + dependencies: 2070 + flatted: 3.3.3 2071 + keyv: 4.5.4 2072 + 2073 + flatted@3.3.3: {} 2074 + 1100 2075 foreground-child@3.3.1: 1101 2076 dependencies: 1102 2077 cross-spawn: 7.0.6 ··· 1105 2080 fsevents@2.3.3: 1106 2081 optional: true 1107 2082 2083 + get-east-asian-width@1.3.1: {} 2084 + 2085 + get-stream@8.0.1: {} 2086 + 2087 + glob-parent@5.1.2: 2088 + dependencies: 2089 + is-glob: 4.0.3 2090 + 2091 + glob-parent@6.0.2: 2092 + dependencies: 2093 + is-glob: 4.0.3 2094 + 1108 2095 glob@10.4.5: 1109 2096 dependencies: 1110 2097 foreground-child: 3.3.1 ··· 1113 2100 minipass: 7.1.2 1114 2101 package-json-from-dist: 1.0.1 1115 2102 path-scurry: 1.11.1 2103 + 2104 + globals@14.0.0: {} 1116 2105 1117 2106 globals@15.15.0: {} 1118 2107 1119 2108 graphemer@1.4.0: {} 1120 2109 2110 + has-flag@4.0.0: {} 2111 + 2112 + human-signals@5.0.0: {} 2113 + 2114 + husky@9.1.7: {} 2115 + 2116 + ignore@5.3.2: {} 2117 + 2118 + ignore@7.0.5: {} 2119 + 2120 + import-fresh@3.3.1: 2121 + dependencies: 2122 + parent-module: 1.0.1 2123 + resolve-from: 4.0.0 2124 + 2125 + imurmurhash@0.1.4: {} 2126 + 2127 + is-extglob@2.1.1: {} 2128 + 1121 2129 is-fullwidth-code-point@3.0.0: {} 1122 2130 2131 + is-fullwidth-code-point@4.0.0: {} 2132 + 2133 + is-fullwidth-code-point@5.1.0: 2134 + dependencies: 2135 + get-east-asian-width: 1.3.1 2136 + 2137 + is-glob@4.0.3: 2138 + dependencies: 2139 + is-extglob: 2.1.1 2140 + 2141 + is-number@7.0.0: {} 2142 + 2143 + is-stream@3.0.0: {} 2144 + 1123 2145 isexe@2.0.0: {} 1124 2146 1125 2147 iso-datestring-validator@2.2.2: {} ··· 1132 2154 1133 2155 joycon@3.1.1: {} 1134 2156 2157 + js-yaml@4.1.0: 2158 + dependencies: 2159 + argparse: 2.0.1 2160 + 2161 + json-buffer@3.0.1: {} 2162 + 2163 + json-schema-traverse@0.4.1: {} 2164 + 2165 + json-stable-stringify-without-jsonify@1.0.1: {} 2166 + 2167 + keyv@4.5.4: 2168 + dependencies: 2169 + json-buffer: 3.0.1 2170 + 2171 + levn@0.4.1: 2172 + dependencies: 2173 + prelude-ls: 1.2.1 2174 + type-check: 0.4.0 2175 + 1135 2176 lilconfig@3.1.3: {} 1136 2177 1137 2178 lines-and-columns@1.2.4: {} 1138 2179 2180 + lint-staged@15.5.2: 2181 + dependencies: 2182 + chalk: 5.6.2 2183 + commander: 13.1.0 2184 + debug: 4.4.0 2185 + execa: 8.0.1 2186 + lilconfig: 3.1.3 2187 + listr2: 8.3.3 2188 + micromatch: 4.0.8 2189 + pidtree: 0.6.0 2190 + string-argv: 0.3.2 2191 + yaml: 2.8.1 2192 + transitivePeerDependencies: 2193 + - supports-color 2194 + 2195 + listr2@8.3.3: 2196 + dependencies: 2197 + cli-truncate: 4.0.0 2198 + colorette: 2.0.20 2199 + eventemitter3: 5.0.1 2200 + log-update: 6.1.0 2201 + rfdc: 1.4.1 2202 + wrap-ansi: 9.0.2 2203 + 1139 2204 load-tsconfig@0.2.5: {} 1140 2205 2206 + locate-path@6.0.0: 2207 + dependencies: 2208 + p-locate: 5.0.0 2209 + 2210 + lodash.merge@4.6.2: {} 2211 + 1141 2212 lodash.sortby@4.7.0: {} 1142 2213 2214 + log-update@6.1.0: 2215 + dependencies: 2216 + ansi-escapes: 7.0.0 2217 + cli-cursor: 5.0.0 2218 + slice-ansi: 7.1.2 2219 + strip-ansi: 7.1.0 2220 + wrap-ansi: 9.0.2 2221 + 1143 2222 lru-cache@10.4.3: {} 1144 2223 1145 2224 luxon@3.6.0: {} 1146 2225 1147 2226 make-error@1.3.6: {} 1148 2227 2228 + merge-stream@2.0.0: {} 2229 + 2230 + merge2@1.4.1: {} 2231 + 2232 + micromatch@4.0.8: 2233 + dependencies: 2234 + braces: 3.0.3 2235 + picomatch: 2.3.1 2236 + 2237 + mimic-fn@4.0.0: {} 2238 + 2239 + mimic-function@5.0.1: {} 2240 + 2241 + minimatch@3.1.2: 2242 + dependencies: 2243 + brace-expansion: 1.1.12 2244 + 1149 2245 minimatch@9.0.5: 1150 2246 dependencies: 1151 - brace-expansion: 4.0.1 2247 + brace-expansion: 2.0.2 1152 2248 1153 2249 minipass@7.1.2: {} 1154 2250 ··· 1162 2258 object-assign: 4.1.1 1163 2259 thenify-all: 1.6.0 1164 2260 2261 + natural-compare@1.4.0: {} 2262 + 2263 + npm-run-path@5.3.0: 2264 + dependencies: 2265 + path-key: 4.0.0 2266 + 1165 2267 object-assign@4.1.1: {} 1166 2268 2269 + onetime@6.0.0: 2270 + dependencies: 2271 + mimic-fn: 4.0.0 2272 + 2273 + onetime@7.0.0: 2274 + dependencies: 2275 + mimic-function: 5.0.1 2276 + 2277 + optionator@0.9.4: 2278 + dependencies: 2279 + deep-is: 0.1.4 2280 + fast-levenshtein: 2.0.6 2281 + levn: 0.4.1 2282 + prelude-ls: 1.2.1 2283 + type-check: 0.4.0 2284 + word-wrap: 1.2.5 2285 + 2286 + p-limit@3.1.0: 2287 + dependencies: 2288 + yocto-queue: 0.1.0 2289 + 2290 + p-locate@5.0.0: 2291 + dependencies: 2292 + p-limit: 3.1.0 2293 + 1167 2294 package-json-from-dist@1.0.1: {} 1168 2295 2296 + parent-module@1.0.1: 2297 + dependencies: 2298 + callsites: 3.1.0 2299 + 2300 + path-exists@4.0.0: {} 2301 + 1169 2302 path-key@3.1.1: {} 2303 + 2304 + path-key@4.0.0: {} 1170 2305 1171 2306 path-scurry@1.11.1: 1172 2307 dependencies: ··· 1175 2310 1176 2311 picocolors@1.1.1: {} 1177 2312 2313 + picomatch@2.3.1: {} 2314 + 1178 2315 picomatch@4.0.2: {} 1179 2316 2317 + pidtree@0.6.0: {} 2318 + 1180 2319 pirates@4.0.7: {} 1181 2320 1182 - postcss-load-config@6.0.1: 2321 + postcss-load-config@6.0.1(yaml@2.8.1): 1183 2322 dependencies: 1184 2323 lilconfig: 3.1.3 2324 + optionalDependencies: 2325 + yaml: 2.8.1 2326 + 2327 + prelude-ls@1.2.1: {} 2328 + 2329 + prettier-linter-helpers@1.0.0: 2330 + dependencies: 2331 + fast-diff: 1.3.0 2332 + 2333 + prettier@3.6.2: {} 1185 2334 1186 2335 punycode@2.3.1: {} 1187 2336 2337 + queue-microtask@1.2.3: {} 2338 + 1188 2339 readdirp@4.1.2: {} 1189 2340 2341 + resolve-from@4.0.0: {} 2342 + 1190 2343 resolve-from@5.0.0: {} 2344 + 2345 + restore-cursor@5.1.0: 2346 + dependencies: 2347 + onetime: 7.0.0 2348 + signal-exit: 4.1.0 2349 + 2350 + reusify@1.1.0: {} 2351 + 2352 + rfdc@1.4.1: {} 1191 2353 1192 2354 rollup@4.39.0: 1193 2355 dependencies: ··· 1215 2377 '@rollup/rollup-win32-x64-msvc': 4.39.0 1216 2378 fsevents: 2.3.3 1217 2379 2380 + run-parallel@1.2.0: 2381 + dependencies: 2382 + queue-microtask: 1.2.3 2383 + 2384 + semver@7.7.2: {} 2385 + 1218 2386 shebang-command@2.0.0: 1219 2387 dependencies: 1220 2388 shebang-regex: 3.0.0 ··· 1223 2391 1224 2392 signal-exit@4.1.0: {} 1225 2393 2394 + slice-ansi@5.0.0: 2395 + dependencies: 2396 + ansi-styles: 6.2.1 2397 + is-fullwidth-code-point: 4.0.0 2398 + 2399 + slice-ansi@7.1.2: 2400 + dependencies: 2401 + ansi-styles: 6.2.1 2402 + is-fullwidth-code-point: 5.1.0 2403 + 1226 2404 source-map@0.8.0-beta.0: 1227 2405 dependencies: 1228 2406 whatwg-url: 7.1.0 2407 + 2408 + string-argv@0.3.2: {} 1229 2409 1230 2410 string-width@4.2.3: 1231 2411 dependencies: ··· 1239 2419 emoji-regex: 9.2.2 1240 2420 strip-ansi: 7.1.0 1241 2421 2422 + string-width@7.2.0: 2423 + dependencies: 2424 + emoji-regex: 10.5.0 2425 + get-east-asian-width: 1.3.1 2426 + strip-ansi: 7.1.0 2427 + 1242 2428 strip-ansi@6.0.1: 1243 2429 dependencies: 1244 2430 ansi-regex: 5.0.1 ··· 1247 2433 dependencies: 1248 2434 ansi-regex: 6.1.0 1249 2435 2436 + strip-final-newline@3.0.0: {} 2437 + 2438 + strip-json-comments@3.1.1: {} 2439 + 1250 2440 sucrase@3.35.0: 1251 2441 dependencies: 1252 2442 '@jridgewell/gen-mapping': 0.3.8 ··· 1257 2447 pirates: 4.0.7 1258 2448 ts-interface-checker: 0.1.13 1259 2449 2450 + supports-color@7.2.0: 2451 + dependencies: 2452 + has-flag: 4.0.0 2453 + 2454 + synckit@0.11.11: 2455 + dependencies: 2456 + '@pkgr/core': 0.2.9 2457 + 1260 2458 thenify-all@1.6.0: 1261 2459 dependencies: 1262 2460 thenify: 3.3.1 ··· 1274 2472 1275 2473 tlds@1.256.0: {} 1276 2474 2475 + to-regex-range@5.0.1: 2476 + dependencies: 2477 + is-number: 7.0.0 2478 + 1277 2479 tr46@1.0.1: 1278 2480 dependencies: 1279 2481 punycode: 2.3.1 1280 2482 1281 2483 tree-kill@1.2.2: {} 2484 + 2485 + ts-api-utils@2.1.0(typescript@5.8.2): 2486 + dependencies: 2487 + typescript: 5.8.2 1282 2488 1283 2489 ts-interface-checker@0.1.13: {} 1284 2490 ··· 1300 2506 v8-compile-cache-lib: 3.0.1 1301 2507 yn: 3.1.1 1302 2508 1303 - tsup@8.4.0(typescript@5.8.2): 2509 + tsup@8.4.0(typescript@5.8.2)(yaml@2.8.1): 1304 2510 dependencies: 1305 2511 bundle-require: 5.1.0(esbuild@0.25.2) 1306 2512 cac: 6.7.14 ··· 1310 2516 esbuild: 0.25.2 1311 2517 joycon: 3.1.1 1312 2518 picocolors: 1.1.1 1313 - postcss-load-config: 6.0.1 2519 + postcss-load-config: 6.0.1(yaml@2.8.1) 1314 2520 resolve-from: 5.0.0 1315 2521 rollup: 4.39.0 1316 2522 source-map: 0.8.0-beta.0 ··· 1326 2532 - tsx 1327 2533 - yaml 1328 2534 2535 + type-check@0.4.0: 2536 + dependencies: 2537 + prelude-ls: 1.2.1 2538 + 1329 2539 typescript@5.8.2: {} 1330 2540 1331 2541 uint8arrays@3.0.0: ··· 1334 2544 1335 2545 undici-types@6.20.0: {} 1336 2546 2547 + uri-js@4.4.1: 2548 + dependencies: 2549 + punycode: 2.3.1 2550 + 1337 2551 v8-compile-cache-lib@3.0.1: {} 1338 2552 1339 2553 webidl-conversions@4.0.2: {} ··· 1348 2562 dependencies: 1349 2563 isexe: 2.0.0 1350 2564 2565 + word-wrap@1.2.5: {} 2566 + 1351 2567 wrap-ansi@7.0.0: 1352 2568 dependencies: 1353 2569 ansi-styles: 4.3.0 ··· 1360 2576 string-width: 5.1.2 1361 2577 strip-ansi: 7.1.0 1362 2578 2579 + wrap-ansi@9.0.2: 2580 + dependencies: 2581 + ansi-styles: 6.2.1 2582 + string-width: 7.2.0 2583 + strip-ansi: 7.1.0 2584 + 1363 2585 ws@8.18.1: {} 1364 2586 2587 + yaml@2.8.1: {} 2588 + 1365 2589 yn@3.1.1: {} 2590 + 2591 + yocto-queue@0.1.0: {} 1366 2592 1367 2593 zod@3.24.2: {}
-2
pnpm-workspace.yaml
··· 1 - overrides: 2 - brace-expansion@>=2.0.0 <=2.0.1: '>=2.0.2'
+50 -12
src/bots/actionBot.ts
··· 1 - import { AtpAgent, AtpAgentOptions } from '@atproto/api'; 2 - import { Logger } from '../utils/logger'; 3 - import type { ActionBot } from '../types/bot'; 1 + import { AtpAgent, AtpAgentOptions } from "@atproto/api"; 2 + import { Logger } from "../utils/logger"; 3 + import type { ActionBot } from "../types/bot"; 4 4 5 5 export class ActionBotAgent extends AtpAgent { 6 - constructor(public opts: AtpAgentOptions, public actionBot: ActionBot) { 6 + constructor( 7 + public opts: AtpAgentOptions, 8 + public actionBot: ActionBot 9 + ) { 7 10 super(opts); 8 11 } 9 12 10 - async doAction(params:any): Promise<void> { 11 - this.actionBot.action(this, params); 13 + async doAction(params?: unknown): Promise<void> { 14 + const correlationId = Logger.startOperation("actionBot.doAction", { 15 + botId: this.actionBot.username || this.actionBot.identifier, 16 + }); 17 + 18 + const startTime = Date.now(); 19 + 20 + try { 21 + await this.actionBot.action(this, params); 22 + Logger.endOperation("actionBot.doAction", startTime, { 23 + correlationId, 24 + botId: this.actionBot.username || this.actionBot.identifier, 25 + }); 26 + } catch (error) { 27 + Logger.error("Action bot execution failed", { 28 + correlationId, 29 + botId: this.actionBot.username || this.actionBot.identifier, 30 + error: error instanceof Error ? error.message : String(error), 31 + }); 32 + throw error; 33 + } 12 34 } 13 35 } 14 36 15 37 export const useActionBotAgent = async (actionBot: ActionBot): Promise<ActionBotAgent | null> => { 38 + const botId = actionBot.username ?? actionBot.identifier; 39 + const correlationId = Logger.startOperation("initializeActionBot", { botId }); 40 + const startTime = Date.now(); 41 + 16 42 const agent = new ActionBotAgent({ service: actionBot.service }, actionBot); 17 - 43 + 18 44 try { 19 - Logger.info(`Initialize action bot ${actionBot.username ?? actionBot.identifier}`); 20 - const login = await agent.login({ identifier: actionBot.identifier, password: actionBot.password! }); 45 + Logger.info("Initializing action bot", { correlationId, botId }); 46 + 47 + const login = await agent.login({ 48 + identifier: actionBot.identifier, 49 + password: actionBot.password!, 50 + }); 51 + 21 52 if (!login.success) { 22 - Logger.warn(`Failed to login action bot ${actionBot.username ?? actionBot.identifier}`); 53 + Logger.warn("Action bot login failed", { correlationId, botId }); 23 54 return null; 24 55 } 56 + 57 + Logger.endOperation("initializeActionBot", startTime, { correlationId, botId }); 25 58 return agent; 26 59 } catch (error) { 27 - Logger.error("Failed to initialize action bot:", `${error}, ${actionBot.username ?? actionBot.identifier}`); 60 + Logger.error("Failed to initialize action bot", { 61 + correlationId, 62 + botId, 63 + error: error.message, 64 + duration: Date.now() - startTime, 65 + }); 28 66 return null; 29 67 } 30 - }; 68 + };
+19 -10
src/bots/cronBot.ts
··· 1 - import { AtpAgent, AtpAgentOptions } from '@atproto/api'; 2 - import { CronJob } from 'cron'; 3 - import { Logger } from '../utils/logger'; 4 - import type { CronBot } from '../types/bot'; 1 + import { AtpAgent, AtpAgentOptions } from "@atproto/api"; 2 + import { CronJob } from "cron"; 3 + import { Logger } from "../utils/logger"; 4 + import type { CronBot } from "../types/bot"; 5 5 6 6 export class CronBotAgent extends AtpAgent { 7 7 public job: CronJob; 8 8 9 - constructor(public opts: AtpAgentOptions, public cronBot: CronBot) { 9 + constructor( 10 + public opts: AtpAgentOptions, 11 + public cronBot: CronBot 12 + ) { 10 13 super(opts); 11 14 12 15 this.job = new CronJob( ··· 14 17 async () => cronBot.action(this), 15 18 cronBot.cronJob.callback, 16 19 false, 17 - cronBot.cronJob.timeZone, 20 + cronBot.cronJob.timeZone 18 21 ); 19 22 } 20 23 } 21 24 22 25 export const useCronBotAgent = async (cronBot: CronBot): Promise<CronBotAgent | null> => { 23 26 const agent = new CronBotAgent({ service: cronBot.service }, cronBot); 24 - 27 + 25 28 try { 26 29 Logger.info(`Initialize cron bot ${cronBot.username ?? cronBot.identifier}`); 27 - const login = await agent.login({ identifier: cronBot.identifier, password: cronBot.password! }); 30 + const login = await agent.login({ 31 + identifier: cronBot.identifier, 32 + password: cronBot.password!, 33 + }); 28 34 if (!login.success) { 29 35 Logger.info(`Failed to login cron bot ${cronBot.username ?? cronBot.identifier}`); 30 36 return null; ··· 32 38 agent.job.start(); 33 39 return agent; 34 40 } catch (error) { 35 - Logger.error("Failed to initialize cron bot:", `${error}, ${cronBot.username ?? cronBot.identifier}`); 41 + Logger.error( 42 + "Failed to initialize cron bot:", 43 + `${error}, ${cronBot.username ?? cronBot.identifier}` 44 + ); 36 45 return null; 37 46 } 38 - }; 47 + };
+94 -74
src/bots/keywordBot.ts
··· 1 - import { AtpAgent, AtpAgentOptions } from '@atproto/api'; 2 - import type { BotReply, KeywordBot } from '../types/bot'; 1 + import { AtpAgent, AtpAgentOptions } from "@atproto/api"; 2 + import type { BotReply, KeywordBot } from "../types/bot"; 3 3 import type { Post, UriCid } from "../types/post"; 4 - import { Logger } from '../utils/logger'; 4 + import { Logger } from "../utils/logger"; 5 5 6 + export class KeywordBotAgent extends AtpAgent { 7 + constructor( 8 + public opts: AtpAgentOptions, 9 + public keywordBot: KeywordBot 10 + ) { 11 + super(opts); 12 + } 6 13 7 - export class KeywordBotAgent extends AtpAgent { 8 - constructor(public opts: AtpAgentOptions, public keywordBot: KeywordBot) { 9 - super(opts); 14 + async likeAndReplyIfFollower(post: Post): Promise<void> { 15 + if (post.authorDid === this.assertDid) { 16 + return; 10 17 } 11 - 12 - async likeAndReplyIfFollower(post: Post): Promise<void> { 13 - if (post.authorDid === this.assertDid) { 14 - return; 15 - } 16 18 17 - const replies = filterBotReplies(post.text, this.keywordBot.replies); 18 - if (replies.length < 1) { 19 - return; 20 - } 19 + const replies = filterBotReplies(post.text, this.keywordBot.replies); 20 + if (replies.length < 1) { 21 + return; 22 + } 21 23 22 - try { 23 - const actorProfile = await this.getProfile({actor: post.authorDid}); 24 + try { 25 + const actorProfile = await this.getProfile({ actor: post.authorDid }); 24 26 25 - if(actorProfile.success) { 26 - 27 - if (!actorProfile.data.viewer?.followedBy) { 28 - return; 29 - } 27 + if (actorProfile.success) { 28 + if (!actorProfile.data.viewer?.followedBy) { 29 + return; 30 + } 30 31 31 - const replyCfg = replies[Math.floor(Math.random() * replies.length)]; 32 - const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)]; 33 - const reply = buildReplyToPost( 34 - { uri: post.rootUri, cid: post.rootCid }, 35 - { uri: post.uri, cid: post.cid }, 36 - message 37 - ); 32 + const replyCfg = replies[Math.floor(Math.random() * replies.length)]; 33 + const message = replyCfg.messages[Math.floor(Math.random() * replyCfg.messages.length)]; 34 + const reply = buildReplyToPost( 35 + { uri: post.rootUri, cid: post.rootCid }, 36 + { uri: post.uri, cid: post.cid }, 37 + message 38 + ); 38 39 39 - await Promise.all([this.like(post.uri, post.cid), this.post(reply)]); 40 - Logger.info(`Replied to post: ${post.uri}`, this.keywordBot.username ?? this.keywordBot.identifier); 41 - } 42 - } catch (error) { 43 - Logger.error("Error while replying:", `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}`); 44 - } 40 + await Promise.all([this.like(post.uri, post.cid), this.post(reply)]); 41 + Logger.info( 42 + `Replied to post: ${post.uri}`, 43 + this.keywordBot.username ?? this.keywordBot.identifier 44 + ); 45 + } 46 + } catch (error) { 47 + Logger.error( 48 + "Error while replying:", 49 + `${error}, ${this.keywordBot.username ?? this.keywordBot.identifier}` 50 + ); 45 51 } 52 + } 46 53 } 47 54 48 - export function buildReplyToPost (root: UriCid, parent: UriCid, message: string) { 49 - return { 50 - $type: "app.bsky.feed.post" as "app.bsky.feed.post", 51 - text: message, 52 - reply: { 53 - "root": root, 54 - "parent": parent 55 - } 56 - }; 55 + export function buildReplyToPost(root: UriCid, parent: UriCid, message: string) { 56 + return { 57 + $type: "app.bsky.feed.post" as const, 58 + text: message, 59 + reply: { 60 + root: root, 61 + parent: parent, 62 + }, 63 + }; 57 64 } 58 65 59 66 export function filterBotReplies(text: string, botReplies: BotReply[]) { 60 - return botReplies.filter(reply => { 61 - const keyword = reply.keyword.toLowerCase(); 62 - const keywordFound = text.toLowerCase().includes(keyword); 63 - if (!keywordFound) { 64 - return false; 65 - } 67 + // Cache the lowercased text to avoid multiple toLowerCase() calls 68 + const lowerText = text.toLowerCase(); 66 69 67 - if (Array.isArray(reply.exclude) && reply.exclude.length > 0) { 68 - for (const excludeWord of reply.exclude) { 69 - if (text.toLowerCase().includes(excludeWord.toLowerCase())) { 70 - return false; 71 - } 72 - } 73 - } 70 + return botReplies.filter(reply => { 71 + // Use cached lowercase comparison 72 + const keyword = reply.keyword.toLowerCase(); 73 + if (!lowerText.includes(keyword)) { 74 + return false; 75 + } 74 76 75 - return true; 76 - }); 77 - } 77 + // Early return if no exclusions 78 + if (!Array.isArray(reply.exclude) || reply.exclude.length === 0) { 79 + return true; 80 + } 78 81 79 - export const useKeywordBotAgent = async (keywordBot: KeywordBot): Promise<KeywordBotAgent | null> => { 80 - const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot); 82 + // Use some() for early exit on first match 83 + const hasExcludedWord = reply.exclude.some(excludeWord => 84 + lowerText.includes(excludeWord.toLowerCase()) 85 + ); 81 86 82 - try { 83 - const login = await agent.login({ identifier: keywordBot.identifier, password: keywordBot.password! }); 87 + return !hasExcludedWord; 88 + }); 89 + } 84 90 85 - Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`); 91 + export const useKeywordBotAgent = async ( 92 + keywordBot: KeywordBot 93 + ): Promise<KeywordBotAgent | null> => { 94 + const agent = new KeywordBotAgent({ service: keywordBot.service }, keywordBot); 86 95 87 - if (!login.success) { 88 - Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`); 89 - return null; 90 - } 96 + try { 97 + const login = await agent.login({ 98 + identifier: keywordBot.identifier, 99 + password: keywordBot.password!, 100 + }); 91 101 92 - return agent; 93 - } catch (error) { 94 - Logger.error("Failed to initialize keyword bot:", `${error}, ${keywordBot.username ?? keywordBot.identifier}`); 95 - return null; 102 + Logger.info(`Initialize keyword bot ${keywordBot.username ?? keywordBot.identifier}`); 103 + 104 + if (!login.success) { 105 + Logger.warn(`Failed to login keyword bot ${keywordBot.username ?? keywordBot.identifier}`); 106 + return null; 96 107 } 97 - }; 108 + 109 + return agent; 110 + } catch (error) { 111 + Logger.error( 112 + "Failed to initialize keyword bot:", 113 + `${error}, ${keywordBot.username ?? keywordBot.identifier}` 114 + ); 115 + return null; 116 + } 117 + };
+12 -11
src/index.ts
··· 1 - export * from "./types/bot" 2 - export * from "./types/message" 3 - export * from "./types/post" 4 - export * from "./bots/actionBot" 5 - export * from "./bots/cronBot" 6 - export * from "./bots/keywordBot" 7 - export * from "./utils/jetstreamSubscription" 8 - export * from "./utils/logger" 9 - export * from "./utils/strings" 10 - export * from "./utils/websocketClient" 11 - export * from "./utils/wsToFeed" 1 + export * from "./types/bot"; 2 + export * from "./types/message"; 3 + export * from "./types/post"; 4 + export * from "./bots/actionBot"; 5 + export * from "./bots/cronBot"; 6 + export * from "./bots/keywordBot"; 7 + export * from "./utils/jetstreamSubscription"; 8 + export * from "./utils/logger"; 9 + export * from "./utils/strings"; 10 + export * from "./utils/websocketClient"; 11 + export * from "./utils/wsToFeed"; 12 + export * from "./utils/healthCheck";
+19 -22
src/types/bot.ts
··· 1 1 import { AtpAgent } from "@atproto/api"; 2 2 3 - 4 3 type Cron = { 5 - scheduleExpression: string; 6 - callback: (() => void) | null; 7 - timeZone: string; 8 - } 4 + scheduleExpression: string; 5 + callback: (() => void) | null; 6 + timeZone: string; 7 + }; 9 8 10 9 export type BotReply = { 11 - keyword: string; 12 - exclude?: string[]; 13 - messages: string[]; 14 - } 10 + keyword: string; 11 + exclude?: string[]; 12 + messages: string[]; 13 + }; 15 14 16 15 export type Bot = { 17 - identifier: string; 18 - password: string; 19 - username?: string; 20 - service: string; 21 - } 16 + identifier: string; 17 + password: string; 18 + username?: string; 19 + service: string; 20 + }; 22 21 23 22 export type ActionBot = Bot & { 24 - action: (agent: AtpAgent, params?: any) => Promise<void>; 25 - } 23 + action: (agent: AtpAgent, params?: unknown) => Promise<void>; 24 + }; 26 25 27 26 export type CronBot = ActionBot & { 28 - cronJob: Cron; 29 - } 30 - 27 + cronJob: Cron; 28 + }; 31 29 32 30 export type KeywordBot = Bot & { 33 - replies: BotReply[]; 34 - } 35 - 31 + replies: BotReply[]; 32 + };
+22 -22
src/types/message.ts
··· 2 2 3 3 /** 4 4 * Represents a message received over WebSocket. 5 - * 5 + * 6 6 * - `did`: The Decentralized Identifier (DID) of the entity that created or owns the data. 7 7 * - `time_us`: A timestamp in microseconds. 8 8 * - `kind`: A string indicating the kind of message. ··· 15 15 * - `'$type'`: The record's type. 16 16 * - `createdAt`: A timestamp indicating when the record was created. 17 17 * - `subject`: A string associated with the record, often referencing another entity. 18 - * - `reply`: Optional object containing `root` and `parent` references (both `UriCid`) 18 + * - `reply`: Optional object containing `root` and `parent` references (both `UriCid`) 19 19 * if the record is a reply to another post. 20 20 * - `text`: The textual content of the record. 21 21 * - `cid`: The content identifier (CID) of the commit. 22 22 */ 23 23 export type WebsocketMessage = { 24 - did: string; 25 - time_us: number; 26 - kind: string; 27 - commit: { 28 - rev: string; 29 - operation: string; 30 - collection: string; 31 - rkey: string; 32 - record: { 33 - '$type': string; 34 - createdAt: string; 35 - subject: string; 36 - reply?: { 37 - root: UriCid; 38 - parent: UriCid; 39 - }; 40 - text: string; 41 - }; 42 - cid: string; 24 + did: string; 25 + time_us: number; 26 + kind: string; 27 + commit: { 28 + rev: string; 29 + operation: string; 30 + collection: string; 31 + rkey: string; 32 + record: { 33 + $type: string; 34 + createdAt: string; 35 + subject: string; 36 + reply?: { 37 + root: UriCid; 38 + parent: UriCid; 39 + }; 40 + text: string; 43 41 }; 44 - }; 42 + cid: string; 43 + }; 44 + };
+12 -12
src/types/post.ts
··· 1 1 export type Post = { 2 - uri: string 3 - cid: string 4 - authorDid: string 5 - authorHandle?: string 6 - text: string 7 - rootUri: string 8 - rootCid: string 9 - createdAt?: Date 10 - } 2 + uri: string; 3 + cid: string; 4 + authorDid: string; 5 + authorHandle?: string; 6 + text: string; 7 + rootUri: string; 8 + rootCid: string; 9 + createdAt?: Date; 10 + }; 11 11 12 12 export type UriCid = { 13 - cid: string 14 - uri: string 15 - } 13 + cid: string; 14 + uri: string; 15 + };
+220
src/utils/healthCheck.ts
··· 1 + import { Logger } from "./logger"; 2 + 3 + export interface HealthStatus { 4 + healthy: boolean; 5 + timestamp: number; 6 + checks: Record<string, boolean>; 7 + metrics: Record<string, number>; 8 + details?: Record<string, unknown>; 9 + } 10 + 11 + export interface HealthCheckOptions { 12 + interval?: number; // milliseconds 13 + timeout?: number; // milliseconds 14 + retries?: number; 15 + } 16 + 17 + /** 18 + * Health monitoring system for bot components. 19 + * Provides health checks and basic metrics collection. 20 + */ 21 + export class HealthMonitor { 22 + private checks = new Map<string, () => Promise<boolean>>(); 23 + private metrics = new Map<string, number>(); 24 + private lastCheckResults = new Map<string, boolean>(); 25 + private checkInterval: NodeJS.Timeout | null = null; 26 + private options: Required<HealthCheckOptions>; 27 + 28 + constructor(options: HealthCheckOptions = {}) { 29 + this.options = { 30 + interval: options.interval || 30000, // 30 seconds 31 + timeout: options.timeout || 5000, // 5 seconds 32 + retries: options.retries || 2, 33 + }; 34 + } 35 + 36 + /** 37 + * Register a health check function. 38 + * @param name - Unique name for the health check 39 + * @param checkFn - Function that returns true if healthy 40 + */ 41 + registerHealthCheck(name: string, checkFn: () => Promise<boolean>) { 42 + this.checks.set(name, checkFn); 43 + Logger.debug(`Registered health check: ${name}`); 44 + } 45 + 46 + /** 47 + * Remove a health check. 48 + * @param name - Name of the health check to remove 49 + */ 50 + unregisterHealthCheck(name: string) { 51 + this.checks.delete(name); 52 + this.lastCheckResults.delete(name); 53 + Logger.debug(`Unregistered health check: ${name}`); 54 + } 55 + 56 + /** 57 + * Set a metric value. 58 + * @param name - Metric name 59 + * @param value - Metric value 60 + */ 61 + setMetric(name: string, value: number) { 62 + this.metrics.set(name, value); 63 + } 64 + 65 + /** 66 + * Increment a counter metric. 67 + * @param name - Metric name 68 + * @param increment - Value to add (default: 1) 69 + */ 70 + incrementMetric(name: string, increment = 1) { 71 + const current = this.metrics.get(name) || 0; 72 + this.metrics.set(name, current + increment); 73 + } 74 + 75 + /** 76 + * Get current metric value. 77 + * @param name - Metric name 78 + * @returns Current value or 0 if not found 79 + */ 80 + getMetric(name: string): number { 81 + return this.metrics.get(name) || 0; 82 + } 83 + 84 + /** 85 + * Get all current metrics. 86 + * @returns Object with all metrics 87 + */ 88 + getAllMetrics(): Record<string, number> { 89 + return Object.fromEntries(this.metrics); 90 + } 91 + 92 + /** 93 + * Run a single health check with timeout and retries. 94 + * @private 95 + */ 96 + private async runHealthCheck(name: string, checkFn: () => Promise<boolean>): Promise<boolean> { 97 + for (let attempt = 0; attempt <= this.options.retries; attempt++) { 98 + try { 99 + const result = await this.withTimeout(checkFn(), this.options.timeout); 100 + if (result) { 101 + return true; 102 + } 103 + } catch (error) { 104 + Logger.debug( 105 + `Health check "${name}" failed (attempt ${attempt + 1}/${this.options.retries + 1}):`, 106 + { error: error.message } 107 + ); 108 + } 109 + } 110 + return false; 111 + } 112 + 113 + /** 114 + * Wrap a promise with a timeout. 115 + * @private 116 + */ 117 + private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> { 118 + return Promise.race([ 119 + promise, 120 + new Promise<T>((_, reject) => 121 + setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) 122 + ), 123 + ]); 124 + } 125 + 126 + /** 127 + * Run all health checks and return the current health status. 128 + */ 129 + async getHealthStatus(): Promise<HealthStatus> { 130 + const timestamp = Date.now(); 131 + const checkResults: Record<string, boolean> = {}; 132 + const details: Record<string, unknown> = {}; 133 + 134 + // Run all health checks 135 + const checkPromises = Array.from(this.checks.entries()).map(async ([name, checkFn]) => { 136 + const result = await this.runHealthCheck(name, checkFn); 137 + checkResults[name] = result; 138 + this.lastCheckResults.set(name, result); 139 + 140 + if (!result) { 141 + details[`${name}_last_failure`] = new Date().toISOString(); 142 + } 143 + 144 + return result; 145 + }); 146 + 147 + await Promise.allSettled(checkPromises); 148 + 149 + // Determine overall health 150 + const healthy = Object.values(checkResults).every(result => result); 151 + 152 + // Get current metrics 153 + const metrics = this.getAllMetrics(); 154 + 155 + return { 156 + healthy, 157 + timestamp, 158 + checks: checkResults, 159 + metrics, 160 + details, 161 + }; 162 + } 163 + 164 + /** 165 + * Start periodic health monitoring. 166 + */ 167 + start() { 168 + if (this.checkInterval) { 169 + this.stop(); 170 + } 171 + 172 + Logger.info(`Starting health monitor with ${this.options.interval}ms interval`); 173 + 174 + this.checkInterval = setInterval(async () => { 175 + try { 176 + const status = await this.getHealthStatus(); 177 + 178 + if (!status.healthy) { 179 + const failedChecks = Object.entries(status.checks) 180 + .filter(([, healthy]) => !healthy) 181 + .map(([name]) => name); 182 + 183 + Logger.warn(`Health check failed`, { 184 + operation: "health_check", 185 + failed_checks: failedChecks, 186 + metrics: status.metrics, 187 + }); 188 + } else { 189 + Logger.debug("Health check passed", { 190 + operation: "health_check", 191 + metrics: status.metrics, 192 + }); 193 + } 194 + } catch (error) { 195 + Logger.error("Error during health check:", { error: error.message }); 196 + } 197 + }, this.options.interval); 198 + } 199 + 200 + /** 201 + * Stop periodic health monitoring. 202 + */ 203 + stop() { 204 + if (this.checkInterval) { 205 + clearInterval(this.checkInterval); 206 + this.checkInterval = null; 207 + Logger.info("Stopped health monitor"); 208 + } 209 + } 210 + 211 + /** 212 + * Get a summary of the last health check results. 213 + */ 214 + getLastCheckSummary(): Record<string, boolean> { 215 + return Object.fromEntries(this.lastCheckResults); 216 + } 217 + } 218 + 219 + // Global health monitor instance 220 + export const healthMonitor = new HealthMonitor();
+55 -52
src/utils/jetstreamSubscription.ts
··· 1 - import WebSocket from 'ws'; 2 - import { WebSocketClient } from './websocketClient'; 3 - import { Logger } from './logger'; 1 + import WebSocket from "ws"; 2 + import { WebSocketClient } from "./websocketClient"; 3 + import { Logger } from "./logger"; 4 4 5 5 /** 6 6 * Represents a subscription to a Jetstream feed over WebSocket. 7 - * 7 + * 8 8 * This class extends `WebSocketClient` to automatically handle reconnections and heartbeats. 9 9 * It invokes a provided callback function whenever a message is received from the Jetstream server. 10 10 */ 11 11 export class JetstreamSubscription extends WebSocketClient { 12 - /** 13 - * Creates a new `JetstreamSubscription`. 14 - * 15 - * @param service - The URL of the Jetstream server to connect to. 16 - * @param interval - The interval (in milliseconds) for reconnect attempts. 17 - * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 18 - */ 19 - constructor( 20 - public service: string, 21 - public interval: number, 22 - private onMessageCallback?: (data: WebSocket.Data) => void 23 - ) { 24 - super({url: service, reconnectInterval: interval}); 25 - } 12 + /** 13 + * Creates a new `JetstreamSubscription`. 14 + * 15 + * @param service - The URL(-Array) of the Jetstream server(s) to connect to. 16 + * @param interval - The interval (in milliseconds) for reconnect attempts. 17 + * @param onMessageCallback - An optional callback function that is invoked whenever a message is received from the server. 18 + */ 19 + constructor( 20 + service: string | string[], 21 + public interval: number, 22 + private onMessageCallback?: (data: WebSocket.Data) => void 23 + ) { 24 + super({ service, reconnectInterval: interval }); 25 + } 26 26 27 - /** 28 - * Called when the WebSocket connection is successfully opened. 29 - * Logs a message indicating that the connection to the Jetstream server has been established. 30 - */ 31 - protected onOpen() { 32 - Logger.info('Connected to Jetstream server.'); 33 - } 27 + /** 28 + * Called when the WebSocket connection is successfully opened. 29 + * Logs a message indicating that the connection to the Jetstream server has been established. 30 + */ 31 + protected onOpen() { 32 + Logger.info("Connected to Jetstream server."); 33 + super.onOpen(); 34 + } 34 35 35 - /** 36 - * Called when a WebSocket message is received. 37 - * 38 - * If an `onMessageCallback` was provided, it is invoked with the received data. 39 - * 40 - * @param data - The data received from the Jetstream server. 41 - */ 42 - protected onMessage(data: WebSocket.Data) { 43 - if (this.onMessageCallback) { 44 - this.onMessageCallback(data); 45 - } 36 + /** 37 + * Called when a WebSocket message is received. 38 + * 39 + * If an `onMessageCallback` was provided, it is invoked with the received data. 40 + * 41 + * @param data - The data received from the Jetstream server. 42 + */ 43 + protected onMessage(data: WebSocket.Data) { 44 + if (this.onMessageCallback) { 45 + this.onMessageCallback(data); 46 46 } 47 + } 47 48 48 - /** 49 - * Called when a WebSocket error occurs. 50 - * Logs the error message indicating that Jetstream encountered an error. 51 - * 52 - * @param error - The error that occurred. 53 - */ 54 - protected onError(error: Error) { 55 - Logger.error('Jetstream encountered an error:', error); 56 - } 49 + /** 50 + * Called when a WebSocket error occurs. 51 + * Logs the error message indicating that Jetstream encountered an error. 52 + * 53 + * @param error - The error that occurred. 54 + */ 55 + protected onError(error: Error) { 56 + Logger.error("Jetstream encountered an error:", error); 57 + super.onError(error); 58 + } 57 59 58 - /** 59 - * Called when the WebSocket connection is closed. 60 - * Logs a message indicating that the Jetstream connection has closed. 61 - */ 62 - protected onClose() { 63 - Logger.info('Jetstream connection closed.'); 64 - } 60 + /** 61 + * Called when the WebSocket connection is closed. 62 + * Logs a message indicating that the Jetstream connection has closed. 63 + */ 64 + protected onClose() { 65 + Logger.info("Jetstream connection closed."); 66 + super.onClose(); 67 + } 65 68 }
+202 -34
src/utils/logger.ts
··· 1 + export enum LogLevel { 2 + DEBUG = 0, 3 + INFO = 1, 4 + WARN = 2, 5 + ERROR = 3, 6 + } 7 + 8 + export interface LogContext { 9 + correlationId?: string; 10 + botId?: string; 11 + operation?: string; 12 + duration?: number; 13 + [key: string]: unknown; 14 + } 15 + 1 16 /** 2 - * A simple logging utility class providing static methods for various log levels. 17 + * A performance-optimized logging utility class providing static methods for various log levels. 3 18 * Each log message is prefixed with a timestamp and log level. 19 + * Supports conditional logging based on log levels and configurable timezone. 4 20 */ 5 21 export class Logger { 6 - /** 7 - * Logs an informational message to the console. 8 - * 9 - * @param message - The message to be logged. 10 - * @param context - Optional additional context (object or string) to log alongside the message. 11 - */ 12 - static info(message: string, context?: object | string) { 13 - console.info(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [INFO]: ${message}`, context || ''); 22 + private static logLevel: LogLevel = LogLevel.INFO; 23 + private static timezone: string = "Europe/Vienna"; 24 + private static correlationId: string | null = null; 25 + 26 + /** 27 + * Generate a new correlation ID for tracking related operations. 28 + */ 29 + static generateCorrelationId(): string { 30 + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 31 + } 32 + 33 + /** 34 + * Set the correlation ID for subsequent log entries. 35 + * @param id - The correlation ID to use, or null to generate a new one 36 + */ 37 + static setCorrelationId(id?: string | null) { 38 + this.correlationId = id || this.generateCorrelationId(); 39 + } 40 + 41 + /** 42 + * Get the current correlation ID. 43 + */ 44 + static getCorrelationId(): string | null { 45 + return this.correlationId; 46 + } 47 + 48 + /** 49 + * Clear the current correlation ID. 50 + */ 51 + static clearCorrelationId() { 52 + this.correlationId = null; 53 + } 54 + 55 + /** 56 + * Set the minimum log level. Messages below this level will not be logged. 57 + * @param level - The minimum log level 58 + */ 59 + static setLogLevel(level: LogLevel) { 60 + this.logLevel = level; 61 + } 62 + 63 + /** 64 + * Set the timezone for log timestamps. 65 + * @param timezone - The timezone string (e.g., "Europe/Vienna", "UTC") 66 + */ 67 + static setTimezone(timezone: string) { 68 + this.timezone = timezone; 69 + } 70 + 71 + /** 72 + * Get the current log level. 73 + */ 74 + static getLogLevel(): LogLevel { 75 + return this.logLevel; 76 + } 77 + 78 + /** 79 + * Generate a formatted timestamp string. 80 + * @private 81 + */ 82 + private static getTimestamp(): string { 83 + return new Date().toLocaleString("de-DE", { timeZone: this.timezone }); 84 + } 85 + 86 + /** 87 + * Internal logging method that checks log level before processing. 88 + * @private 89 + */ 90 + private static log( 91 + level: LogLevel, 92 + levelName: string, 93 + message: string, 94 + context?: LogContext | object | string, 95 + logFn = console.log 96 + ) { 97 + if (level < this.logLevel) { 98 + return; // Skip logging if below threshold 14 99 } 15 100 16 - /** 17 - * Logs a warning message to the console. 18 - * 19 - * @param message - The message to be logged. 20 - * @param context - Optional additional context (object or string) to log alongside the message. 21 - */ 22 - static warn(message: string, context?: object | string) { 23 - console.warn(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [WARNING]: ${message}`, context || ''); 101 + const timestamp = this.getTimestamp(); 102 + let formattedMessage = `${timestamp} [${levelName}]`; 103 + 104 + // Add correlation ID if available 105 + if (this.correlationId) { 106 + formattedMessage += ` [${this.correlationId}]`; 24 107 } 25 108 26 - /** 27 - * Logs an error message to the console. 28 - * 29 - * @param message - The message to be logged. 30 - * @param context - Optional additional context (object or string) to log alongside the message. 31 - */ 32 - static error(message: string, context?: object | string) { 33 - console.error(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [ERROR]: ${message}`, context || ''); 109 + // Add context correlation ID if provided and different from global one 110 + if ( 111 + context && 112 + typeof context === "object" && 113 + "correlationId" in context && 114 + context.correlationId && 115 + context.correlationId !== this.correlationId 116 + ) { 117 + formattedMessage += ` [${context.correlationId}]`; 34 118 } 35 119 36 - /** 37 - * Logs a debug message to the console. 38 - * 39 - * @param message - The message to be logged. 40 - * @param context - Optional additional context (object or string) to log alongside the message. 41 - */ 42 - static debug(message: string, context?: object | string) { 43 - console.debug(`${new Date().toLocaleString("de-DE", {timeZone: "Europe/Vienna"})} [DEBUG]: ${message}`, context || ''); 120 + formattedMessage += `: ${message}`; 121 + 122 + if (context) { 123 + // Create structured log entry for objects 124 + if (typeof context === "object") { 125 + const logEntry = { 126 + timestamp: new Date().toISOString(), 127 + level: levelName, 128 + message, 129 + correlationId: this.correlationId, 130 + ...context, 131 + }; 132 + logFn(formattedMessage, logEntry); 133 + } else { 134 + logFn(formattedMessage, context); 135 + } 136 + } else { 137 + logFn(formattedMessage); 44 138 } 45 - } 139 + } 140 + /** 141 + * Logs an informational message to the console. 142 + * 143 + * @param message - The message to be logged. 144 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 145 + */ 146 + static info(message: string, context?: LogContext | object | string) { 147 + this.log(LogLevel.INFO, "INFO", message, context, console.info); 148 + } 149 + 150 + /** 151 + * Logs a warning message to the console. 152 + * 153 + * @param message - The message to be logged. 154 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 155 + */ 156 + static warn(message: string, context?: LogContext | object | string) { 157 + this.log(LogLevel.WARN, "WARNING", message, context, console.warn); 158 + } 159 + 160 + /** 161 + * Logs an error message to the console. 162 + * 163 + * @param message - The message to be logged. 164 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 165 + */ 166 + static error(message: string, context?: LogContext | object | string) { 167 + this.log(LogLevel.ERROR, "ERROR", message, context, console.error); 168 + } 169 + 170 + /** 171 + * Logs a debug message to the console. 172 + * 173 + * @param message - The message to be logged. 174 + * @param context - Optional additional context (LogContext, object or string) to log alongside the message. 175 + */ 176 + static debug(message: string, context?: LogContext | object | string) { 177 + this.log(LogLevel.DEBUG, "DEBUG", message, context, console.debug); 178 + } 179 + 180 + /** 181 + * Log operation start with timing. 182 + * @param operation - The operation name 183 + * @param context - Additional context 184 + */ 185 + static startOperation(operation: string, context?: LogContext): string { 186 + const correlationId = context?.correlationId || this.generateCorrelationId(); 187 + this.setCorrelationId(correlationId); 188 + 189 + this.info(`Starting operation: ${operation}`, { 190 + operation, 191 + correlationId, 192 + ...context, 193 + }); 194 + 195 + return correlationId; 196 + } 197 + 198 + /** 199 + * Log operation completion with timing. 200 + * @param operation - The operation name 201 + * @param startTime - The start time from Date.now() 202 + * @param context - Additional context 203 + */ 204 + static endOperation(operation: string, startTime: number, context?: LogContext) { 205 + const duration = Date.now() - startTime; 206 + 207 + this.info(`Completed operation: ${operation}`, { 208 + operation, 209 + duration: `${duration}ms`, 210 + ...context, 211 + }); 212 + } 213 + }
+8 -8
src/utils/strings.ts
··· 1 1 /** 2 2 * Returns the given string if it is defined; otherwise returns `undefined`. 3 - * 3 + * 4 4 * @param val - The optional string value to check. 5 5 * @returns The given string if defined, or `undefined` if `val` is falsy. 6 6 */ 7 7 export const maybeStr = (val?: string): string | undefined => { 8 8 if (!val) return undefined; 9 9 return val; 10 - } 10 + }; 11 11 12 12 /** 13 - * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 14 - * 15 - * @param val - The optional string value to parse. 16 - * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 17 - */ 13 + * Parses the given string as an integer if it is defined and a valid integer; otherwise returns `undefined`. 14 + * 15 + * @param val - The optional string value to parse. 16 + * @returns The parsed integer if successful, or `undefined` if the string is falsy or not a valid integer. 17 + */ 18 18 export const maybeInt = (val?: string): number | undefined => { 19 19 if (!val) return undefined; 20 20 const int = parseInt(val, 10); 21 21 if (isNaN(int)) return undefined; 22 22 return int; 23 - } 23 + };
+346 -129
src/utils/websocketClient.ts
··· 1 - import WebSocket from 'ws'; 2 - import { Logger } from './logger'; 1 + import WebSocket from "ws"; 2 + import { Logger } from "./logger"; 3 + import { healthMonitor } from "./healthCheck"; 3 4 4 5 interface WebSocketClientOptions { 5 - /** The URL of the WebSocket server to connect to. */ 6 - url: string; 7 - /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */ 8 - reconnectInterval?: number; 9 - /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */ 10 - pingInterval?: number; 6 + /** The URL of the WebSocket server to connect to. */ 7 + service: string | string[]; 8 + /** The interval in milliseconds to wait before attempting to reconnect when the connection closes. Default is 5000ms. */ 9 + reconnectInterval?: number; 10 + /** The interval in milliseconds for sending ping messages (heartbeats) to keep the connection alive. Default is 10000ms. */ 11 + pingInterval?: number; 12 + /** Maximum number of consecutive reconnection attempts per service. Default is 3. */ 13 + maxReconnectAttempts?: number; 14 + /** Maximum delay between reconnection attempts in milliseconds. Default is 30000ms (30 seconds). */ 15 + maxReconnectDelay?: number; 16 + /** Exponential backoff factor for reconnection delays. Default is 1.5. */ 17 + backoffFactor?: number; 18 + /** Maximum number of attempts to cycle through all services before giving up. Default is 2. */ 19 + maxServiceCycles?: number; 11 20 } 12 21 13 22 /** 14 23 * A WebSocket client that automatically attempts to reconnect upon disconnection 15 24 * and periodically sends ping messages (heartbeats) to ensure the connection remains alive. 16 - * 25 + * 17 26 * Extend this class and override the protected `onOpen`, `onMessage`, `onError`, and `onClose` methods 18 27 * to implement custom handling of WebSocket events. 19 28 */ 20 29 export class WebSocketClient { 21 - private url: string; 22 - private reconnectInterval: number; 23 - private pingInterval: number; 24 - private ws: WebSocket | null = null; 25 - private pingTimeout: NodeJS.Timeout | null = null; 30 + private service: string | string[]; 31 + private reconnectInterval: number; 32 + private pingInterval: number; 33 + private ws: WebSocket | null = null; 34 + private pingTimeout: NodeJS.Timeout | null = null; 35 + private serviceIndex = 0; 36 + private reconnectAttempts = 0; 37 + private serviceCycles = 0; 38 + private maxReconnectAttempts: number; 39 + private maxServiceCycles: number; 40 + private maxReconnectDelay: number; 41 + private backoffFactor: number; 42 + private reconnectTimeout: NodeJS.Timeout | null = null; 43 + private isConnecting = false; 44 + private shouldReconnect = true; 45 + private messageCount = 0; 46 + private lastMessageTime = 0; 47 + private healthCheckName: string; 48 + 49 + /** 50 + * Creates a new instance of `WebSocketClient`. 51 + * 52 + * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval. 53 + */ 54 + constructor(options: WebSocketClientOptions) { 55 + this.service = options.service; 56 + this.reconnectInterval = options.reconnectInterval || 5000; 57 + this.pingInterval = options.pingInterval || 10000; 58 + this.maxReconnectAttempts = options.maxReconnectAttempts || 3; 59 + this.maxServiceCycles = options.maxServiceCycles || 2; 60 + this.maxReconnectDelay = options.maxReconnectDelay || 30000; 61 + this.backoffFactor = options.backoffFactor || 1.5; 62 + 63 + // Generate unique health check name 64 + this.healthCheckName = `websocket_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; 65 + 66 + // Register health check 67 + healthMonitor.registerHealthCheck(this.healthCheckName, async () => { 68 + return this.getConnectionState() === "CONNECTED"; 69 + }); 26 70 27 - /** 28 - * Creates a new instance of `WebSocketClient`. 29 - * 30 - * @param options - Configuration options for the WebSocket client, including URL, reconnect interval, and ping interval. 31 - */ 32 - constructor(options: WebSocketClientOptions) { 33 - this.url = options.url; 34 - this.reconnectInterval = options.reconnectInterval || 5000; 35 - this.pingInterval = options.pingInterval || 10000; 36 - this.run(); 71 + // Initialize metrics 72 + healthMonitor.setMetric(`${this.healthCheckName}_messages_received`, 0); 73 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, 0); 74 + 75 + this.run(); 76 + } 77 + 78 + /** 79 + * Initiates a WebSocket connection to the specified URL. 80 + * 81 + * This method sets up event listeners for `open`, `message`, `error`, and `close` events. 82 + * When the connection opens, it starts the heartbeat mechanism. 83 + * On close, it attempts to reconnect after a specified interval. 84 + */ 85 + private run() { 86 + if (this.isConnecting) { 87 + return; 37 88 } 38 89 39 - /** 40 - * Initiates a WebSocket connection to the specified URL. 41 - * 42 - * This method sets up event listeners for `open`, `message`, `error`, and `close` events. 43 - * When the connection opens, it starts the heartbeat mechanism. 44 - * On close, it attempts to reconnect after a specified interval. 45 - */ 46 - private run() { 47 - this.ws = new WebSocket(this.url); 90 + this.isConnecting = true; 91 + const currentService = Array.isArray(this.service) 92 + ? this.service[this.serviceIndex] 93 + : this.service; 48 94 49 - this.ws.on('open', () => { 50 - Logger.info('WebSocket connected'); 51 - this.startHeartbeat(); 52 - this.onOpen(); 53 - }); 95 + Logger.info(`Attempting to connect to WebSocket: ${currentService}`); 96 + this.ws = new WebSocket(currentService); 54 97 55 - this.ws.on('message', (data: WebSocket.Data) => { 56 - this.onMessage(data); 98 + this.ws.on("open", () => { 99 + Logger.info("WebSocket connected successfully", { 100 + service: this.getCurrentService(), 101 + serviceIndex: this.serviceIndex, 102 + }); 103 + this.isConnecting = false; 104 + this.reconnectAttempts = 0; // Reset on successful connection 105 + this.serviceCycles = 0; // Reset cycles on successful connection 106 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 107 + this.startHeartbeat(); 108 + this.onOpen(); 109 + }); 110 + 111 + this.ws.on("message", (data: WebSocket.Data) => { 112 + this.messageCount++; 113 + this.lastMessageTime = Date.now(); 114 + healthMonitor.incrementMetric(`${this.healthCheckName}_messages_received`); 115 + this.onMessage(data); 116 + }); 117 + 118 + this.ws.on("error", error => { 119 + Logger.error("WebSocket error:", error); 120 + this.isConnecting = false; 121 + this.onError(error); 122 + }); 123 + 124 + this.ws.on("close", (code, reason) => { 125 + Logger.info(`WebSocket disconnected. Code: ${code}, Reason: ${reason.toString()}`); 126 + this.isConnecting = false; 127 + this.stopHeartbeat(); 128 + this.onClose(); 129 + 130 + if (this.shouldReconnect) { 131 + this.scheduleReconnect(); 132 + } 133 + }); 134 + } 135 + 136 + /** 137 + * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 138 + * It clears all event listeners on the old WebSocket and initiates a new connection. 139 + */ 140 + private scheduleReconnect() { 141 + this.reconnectAttempts++; 142 + healthMonitor.setMetric(`${this.healthCheckName}_reconnect_attempts`, this.reconnectAttempts); 143 + 144 + // Check if we should try the next service 145 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { 146 + if (this.shouldTryNextService()) { 147 + this.moveToNextService(); 148 + return; // Try next service immediately 149 + } else { 150 + Logger.error("All services exhausted after maximum cycles", { 151 + totalServices: Array.isArray(this.service) ? this.service.length : 1, 152 + maxServiceCycles: this.maxServiceCycles, 153 + serviceCycles: this.serviceCycles, 57 154 }); 155 + return; // Give up entirely 156 + } 157 + } 58 158 59 - this.ws.on('error', (error) => { 60 - Logger.error('WebSocket error:', error); 61 - this.onError(error); 62 - }); 159 + const delay = Math.min( 160 + this.reconnectInterval * Math.pow(this.backoffFactor, this.reconnectAttempts - 1), 161 + this.maxReconnectDelay 162 + ); 63 163 64 - this.ws.on('close', () => { 65 - Logger.info('WebSocket disconnected'); 66 - this.stopHeartbeat(); 67 - this.onClose(); 68 - this.reconnect(); 69 - }); 164 + Logger.info( 165 + `Scheduling reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} for service`, 166 + { 167 + service: this.getCurrentService(), 168 + serviceIndex: this.serviceIndex, 169 + delay: `${delay}ms`, 170 + } 171 + ); 172 + 173 + if (this.reconnectTimeout) { 174 + clearTimeout(this.reconnectTimeout); 175 + } 176 + 177 + this.reconnectTimeout = setTimeout(() => { 178 + this.cleanup(); 179 + this.run(); 180 + }, delay); 181 + } 182 + 183 + /** 184 + * Check if we should try the next service in the array. 185 + */ 186 + private shouldTryNextService(): boolean { 187 + if (!Array.isArray(this.service)) { 188 + return false; // Single service, can't switch 70 189 } 71 190 72 - /** 73 - * Attempts to reconnect to the WebSocket server after the specified `reconnectInterval`. 74 - * It clears all event listeners on the old WebSocket and initiates a new connection. 75 - */ 76 - private reconnect() { 77 - if (this.ws) { 78 - this.ws.removeAllListeners(); 79 - this.ws = null; 80 - } 191 + return this.serviceCycles < this.maxServiceCycles; 192 + } 81 193 82 - setTimeout(() => this.run(), this.reconnectInterval); 194 + /** 195 + * Move to the next service in the array and reset reconnection attempts. 196 + */ 197 + private moveToNextService() { 198 + if (!Array.isArray(this.service)) { 199 + return; 83 200 } 84 201 85 - /** 86 - * Starts sending periodic ping messages to the server. 87 - * 88 - * This function uses `setInterval` to send a ping at the configured `pingInterval`. 89 - * If the WebSocket is not open, pings are not sent. 90 - */ 91 - private startHeartbeat() { 92 - this.pingTimeout = setInterval(() => { 93 - if (this.ws && this.ws.readyState === WebSocket.OPEN) { 94 - this.ws.ping(); 95 - } 96 - }, this.pingInterval); 202 + const previousIndex = this.serviceIndex; 203 + this.serviceIndex = (this.serviceIndex + 1) % this.service.length; 204 + 205 + // If we've gone through all services once, increment the cycle counter 206 + if (this.serviceIndex === 0) { 207 + this.serviceCycles++; 97 208 } 98 209 99 - /** 100 - * Stops sending heartbeat pings by clearing the ping interval. 101 - */ 102 - private stopHeartbeat() { 103 - if (this.pingTimeout) { 104 - clearInterval(this.pingTimeout); 105 - this.pingTimeout = null; 106 - } 210 + this.reconnectAttempts = 0; // Reset attempts for the new service 211 + 212 + Logger.info("Switching to next service", { 213 + previousService: this.service[previousIndex], 214 + previousIndex, 215 + newService: this.getCurrentService(), 216 + newIndex: this.serviceIndex, 217 + serviceCycle: this.serviceCycles, 218 + }); 219 + 220 + // Try the new service immediately 221 + this.cleanup(); 222 + this.run(); 223 + } 224 + 225 + private cleanup() { 226 + if (this.ws) { 227 + this.ws.removeAllListeners(); 228 + if (this.ws.readyState === WebSocket.OPEN) { 229 + this.ws.close(); 230 + } 231 + this.ws = null; 107 232 } 108 233 109 - /** 110 - * Called when the WebSocket connection is successfully opened. 111 - * 112 - * Override this method in a subclass to implement custom logic on connection. 113 - */ 114 - protected onOpen() { 115 - // Custom logic for connection open 234 + if (this.reconnectTimeout) { 235 + clearTimeout(this.reconnectTimeout); 236 + this.reconnectTimeout = null; 116 237 } 238 + } 117 239 118 - /** 119 - * Called when a WebSocket message is received. 120 - * 121 - * @param data - The data received from the WebSocket server. 122 - * 123 - * Override this method in a subclass to implement custom message handling. 124 - */ 125 - protected onMessage(data: WebSocket.Data) { 126 - // Custom logic for handling received messages 240 + /** 241 + * Starts sending periodic ping messages to the server. 242 + * 243 + * This function uses `setInterval` to send a ping at the configured `pingInterval`. 244 + * If the WebSocket is not open, pings are not sent. 245 + */ 246 + private startHeartbeat() { 247 + this.pingTimeout = setInterval(() => { 248 + if (this.ws && this.ws.readyState === WebSocket.OPEN) { 249 + this.ws.ping(); 250 + } 251 + }, this.pingInterval); 252 + } 253 + 254 + /** 255 + * Stops sending heartbeat pings by clearing the ping interval. 256 + */ 257 + private stopHeartbeat() { 258 + if (this.pingTimeout) { 259 + clearInterval(this.pingTimeout); 260 + this.pingTimeout = null; 127 261 } 262 + } 128 263 129 - /** 130 - * Called when a WebSocket error occurs. 131 - * 132 - * @param error - The error that occurred. 133 - * 134 - * Override this method in a subclass to implement custom error handling. 135 - */ 136 - protected onError(error: Error) { 137 - // Custom logic for handling errors 264 + /** 265 + * Called when the WebSocket connection is successfully opened. 266 + * 267 + * Override this method in a subclass to implement custom logic on connection. 268 + */ 269 + protected onOpen() { 270 + // Custom logic for connection open 271 + } 272 + 273 + /** 274 + * Called when a WebSocket message is received. 275 + * 276 + * @param data - The data received from the WebSocket server. 277 + * 278 + * Override this method in a subclass to implement custom message handling. 279 + */ 280 + protected onMessage(_data: WebSocket.Data) { 281 + // Custom logic for handling received messages 282 + } 283 + 284 + /** 285 + * Called when a WebSocket error occurs. 286 + * 287 + * @param error - The error that occurred. 288 + * 289 + * Override this method in a subclass to implement custom error handling. 290 + * Note: Service switching is now handled in the reconnection logic, not here. 291 + */ 292 + protected onError(_error: Error) { 293 + // Custom logic for handling errors - override in subclasses 294 + // Service switching is handled automatically in scheduleReconnect() 295 + } 296 + 297 + /** 298 + * Called when the WebSocket connection is closed. 299 + * 300 + * Override this method in a subclass to implement custom logic on disconnection. 301 + */ 302 + protected onClose() { 303 + // Custom logic for handling connection close 304 + } 305 + 306 + /** 307 + * Sends data to the connected WebSocket server, if the connection is open. 308 + * 309 + * @param data - The data to send. 310 + */ 311 + public send(data: string | Buffer | ArrayBuffer | Buffer[]) { 312 + if (this.ws && this.ws.readyState === WebSocket.OPEN) { 313 + this.ws.send(data); 138 314 } 315 + } 139 316 140 - /** 141 - * Called when the WebSocket connection is closed. 142 - * 143 - * Override this method in a subclass to implement custom logic on disconnection. 144 - */ 145 - protected onClose() { 146 - // Custom logic for handling connection close 317 + /** 318 + * Closes the WebSocket connection gracefully. 319 + */ 320 + public close() { 321 + this.shouldReconnect = false; 322 + this.stopHeartbeat(); 323 + 324 + if (this.reconnectTimeout) { 325 + clearTimeout(this.reconnectTimeout); 326 + this.reconnectTimeout = null; 147 327 } 148 328 149 - /** 150 - * Sends data to the connected WebSocket server, if the connection is open. 151 - * 152 - * @param data - The data to send. 153 - */ 154 - public send(data: any) { 155 - if (this.ws && this.ws.readyState === WebSocket.OPEN) { 156 - this.ws.send(data); 157 - } 329 + if (this.ws) { 330 + this.ws.close(); 158 331 } 159 332 160 - /** 161 - * Closes the WebSocket connection gracefully. 162 - */ 163 - public close() { 164 - if (this.ws) { 165 - this.ws.close(); 166 - } 333 + // Unregister health check when closing 334 + healthMonitor.unregisterHealthCheck(this.healthCheckName); 335 + } 336 + 337 + public getConnectionState(): string { 338 + if (!this.ws) return "DISCONNECTED"; 339 + 340 + switch (this.ws.readyState) { 341 + case WebSocket.CONNECTING: 342 + return "CONNECTING"; 343 + case WebSocket.OPEN: 344 + return "CONNECTED"; 345 + case WebSocket.CLOSING: 346 + return "CLOSING"; 347 + case WebSocket.CLOSED: 348 + return "DISCONNECTED"; 349 + default: 350 + return "UNKNOWN"; 167 351 } 168 - } 352 + } 353 + 354 + public getReconnectAttempts(): number { 355 + return this.reconnectAttempts; 356 + } 357 + 358 + public getServiceCycles(): number { 359 + return this.serviceCycles; 360 + } 361 + 362 + public getServiceIndex(): number { 363 + return this.serviceIndex; 364 + } 365 + 366 + public getAllServices(): string[] { 367 + return Array.isArray(this.service) ? [...this.service] : [this.service]; 368 + } 369 + 370 + public getCurrentService(): string { 371 + return Array.isArray(this.service) ? this.service[this.serviceIndex] : this.service; 372 + } 373 + 374 + public getMessageCount(): number { 375 + return this.messageCount; 376 + } 377 + 378 + public getLastMessageTime(): number { 379 + return this.lastMessageTime; 380 + } 381 + 382 + public getHealthCheckName(): string { 383 + return this.healthCheckName; 384 + } 385 + }
+26 -20
src/utils/wsToFeed.ts
··· 1 - import WebSocket from 'ws'; 1 + import WebSocket from "ws"; 2 2 import { Post } from "../types/post"; 3 - import { WebsocketMessage } from '../types/message'; 4 - ; 5 - 3 + import { WebsocketMessage } from "../types/message"; 6 4 /** 7 5 * Converts a raw WebSocket message into a `FeedEntry` object, if possible. 8 - * 6 + * 9 7 * This function checks if the incoming WebSocket data is structured like a feed commit message 10 8 * with the required properties for a created post. If the data matches the expected shape, 11 9 * it extracts and returns a `FeedEntry` object. Otherwise, it returns `null`. 12 - * 10 + * 13 11 * @param data - The raw WebSocket data. 14 12 * @returns A `FeedEntry` object if the data represents a newly created post, otherwise `null`. 15 13 */ 16 14 export function websocketToFeedEntry(data: WebSocket.Data): Post | null { 17 - const message = data as WebsocketMessage; 18 - if(!message.commit || !message.commit.record || !message.commit.record['$type'] || !message.did || !message.commit.cid || !message.commit.rkey || message.commit.operation !== "create") { 19 - return null; 20 - } 21 - const messageUri = `at://${message.did}/${message.commit.record['$type']}/${message.commit.rkey}`; 22 - return { 23 - cid: message.commit.cid, 24 - uri: messageUri, 25 - authorDid: message.did, 26 - text: message.commit.record.text, 27 - rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid, 28 - rootUri: message.commit.record.reply?.root.uri ?? messageUri, 29 - }; 30 - } 15 + const message = data as WebsocketMessage; 16 + if ( 17 + !message.commit || 18 + !message.commit.record || 19 + !message.commit.record["$type"] || 20 + !message.did || 21 + !message.commit.cid || 22 + !message.commit.rkey || 23 + message.commit.operation !== "create" 24 + ) { 25 + return null; 26 + } 27 + const messageUri = `at://${message.did}/${message.commit.record["$type"]}/${message.commit.rkey}`; 28 + return { 29 + cid: message.commit.cid, 30 + uri: messageUri, 31 + authorDid: message.did, 32 + text: message.commit.record.text, 33 + rootCid: message.commit.record.reply?.root.cid ?? message.commit.cid, 34 + rootUri: message.commit.record.reply?.root.uri ?? messageUri, 35 + }; 36 + }