Add FeedOrchestrator that coordinates fetch→parse→dedup→store pipeline: - FeedSource type for managing RSS/Atom feed configurations - Feed source CRUD operations in IStorage interface - Database schema migration for feed_sources table - Exponential backoff retry with configurable delays - Per-feed poll intervals with health tracking - Concurrency-limited parallel feed processing - ProcessResult and FeedHealth interfaces for status monitoring Files added: - orchestrator/orchestrator.ts - main orchestrator class - orchestrator/scheduler.ts - backoff calculation utilities - orchestrator/index.ts - module exports - orchestrator/orchestrator.test.ts - comprehensive test suite Files modified: - interfaces/feed.types.ts - add FeedSource type - interfaces/storage.interface.ts - extend with feed source methods - infrastructure/db/database.ts - add FeedSourceTable interface - infrastructure/db/schema.ts - add feed_sources table migration - modules/storage/storage.ts - implement feed source CRUD - modules/storage/storage.test.ts - add feed source tests
66 lines
1.6 KiB
TypeScript
66 lines
1.6 KiB
TypeScript
import type { FeedItem } from '../../interfaces/feed.types.js';
|
|
|
|
export class TerminalFormatter {
|
|
format(items: FeedItem[]): string {
|
|
if (items.length === 0) {
|
|
return '\n No items to display.\n';
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
lines.push('');
|
|
lines.push(` ${this.bold(`Found ${items.length} item${items.length === 1 ? '' : 's'}`)}`);
|
|
lines.push('');
|
|
|
|
items.forEach((item, index) => {
|
|
const number = `${index + 1}.`.padStart(3);
|
|
lines.push(` ${this.dim(number)} ${this.cyan(item.source)}`);
|
|
lines.push(` ${this.bold(item.title)}`);
|
|
lines.push(` ${this.dim(this.formatDate(item.publishedAt))}`);
|
|
lines.push(` ${this.blue(item.url)}`);
|
|
|
|
if (item.summary) {
|
|
const truncated = this.truncate(item.summary, 80);
|
|
lines.push(` ${this.dim(truncated)}`);
|
|
}
|
|
|
|
lines.push('');
|
|
});
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
private formatDate(date: Date): string {
|
|
return date.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
private truncate(text: string, maxLength: number): string {
|
|
if (text.length <= maxLength) {
|
|
return text;
|
|
}
|
|
return text.substring(0, maxLength - 3) + '...';
|
|
}
|
|
|
|
// ANSI color codes
|
|
private bold(text: string): string {
|
|
return `\x1b[1m${text}\x1b[0m`;
|
|
}
|
|
|
|
private dim(text: string): string {
|
|
return `\x1b[2m${text}\x1b[0m`;
|
|
}
|
|
|
|
private cyan(text: string): string {
|
|
return `\x1b[36m${text}\x1b[0m`;
|
|
}
|
|
|
|
private blue(text: string): string {
|
|
return `\x1b[34m${text}\x1b[0m`;
|
|
}
|
|
}
|