+4
.husky/pre-commit
+4
.husky/pre-commit
+11
.prettierrc
+11
.prettierrc
+77
-5
README.md
+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
+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
+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
+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
-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
+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
-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
+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
+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
+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: {}
+50
-12
src/bots/actionBot.ts
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
}