refactor: use base http client for notifications, parser, autocomp

This commit is contained in:
Sam Chau
2026-01-19 02:26:12 +10:30
parent f6d99bc267
commit 8d3e20d3c3
5 changed files with 232 additions and 180 deletions

View File

@@ -1,6 +1,7 @@
import { logger } from '$logger/logger.ts';
import type { Notification } from '../types.ts';
import type { Notifier } from './Notifier.ts';
import { getWebhookClient } from './webhookClient.ts';
/**
* Base class for HTTP-based notification services (webhooks)
@@ -9,7 +10,6 @@ import type { Notifier } from './Notifier.ts';
export abstract class BaseHttpNotifier implements Notifier {
private lastSentAt: Date | null = null;
private readonly minInterval: number = 1000; // 1 second between notifications
private readonly timeout: number = 10000; // 10 second timeout
/**
* Get the webhook URL for this service
@@ -47,43 +47,14 @@ export abstract class BaseHttpNotifier implements Notifier {
const payload = this.formatPayload(notification);
const url = this.getWebhookUrl();
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
await getWebhookClient().sendWebhook(url, payload);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: controller.signal
});
await logger.debug(`Notification sent`, {
source: this.getName(),
meta: { type: notification.type }
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
await logger.debug(`Notification sent`, {
source: this.getName(),
meta: { type: notification.type }
});
this.lastSentAt = new Date();
} catch (error) {
clearTimeout(timeoutId);
// Handle timeout
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
this.lastSentAt = new Date();
} catch (error) {
// Log error but don't throw (fire-and-forget)
await logger.error(`Failed to send notification`, {

View File

@@ -0,0 +1,42 @@
/**
* Shared HTTP client for webhook-based notifications
* Uses BaseHttpClient for connection pooling
*/
import { BaseHttpClient } from '../../utils/http/client.ts';
/**
* Webhook HTTP client
* Extends BaseHttpClient with webhook-specific settings:
* - No retries (webhooks should either work or not)
* - 10 second timeout
*/
class WebhookClient extends BaseHttpClient {
constructor() {
// Empty base URL - we pass full webhook URLs as paths
super('', {
timeout: 10000,
retries: 0
});
}
/**
* POST to a webhook URL
*/
sendWebhook<T = void>(url: string, payload: unknown): Promise<T> {
return this.post<T>(url, payload);
}
}
// Singleton instance - lazy initialized
let webhookClient: WebhookClient | null = null;
/**
* Get the shared webhook client
*/
export function getWebhookClient(): WebhookClient {
if (!webhookClient) {
webhookClient = new WebhookClient();
}
return webhookClient;
}

View File

@@ -1,6 +1,7 @@
import { logger } from '$logger/logger.ts';
import type { DiscordConfig, Notification } from '../../types.ts';
import { Colors, type DiscordEmbed } from './embed.ts';
import { getWebhookClient } from '../../base/webhookClient.ts';
const RATE_LIMIT_DELAY = 1000; // 1 second between messages
@@ -104,30 +105,10 @@ export class DiscordNotifier {
*/
private async sendWebhook(payload: unknown): Promise<void> {
const payloadObj = payload as { embeds?: unknown[] };
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(this.config.webhook_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
await getWebhookClient().sendWebhook(this.config.webhook_url, payload);
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Request timeout');
}
const embedCharCounts = payloadObj.embeds?.map((e, i) => `${i}:${getEmbedCharCount(e as DiscordEmbed)}`).join(', ') || 'none';
await logger.error('Failed to send notification', {
source: this.getName(),