2Branches0Tags
GL
glucryptoSync production with master template updates
075e1302 months ago14Commits
typescript
import type { ImportantDatesRoundupConfig } from "./config"; type GatewayImportantDate = { id: string; series: string; category: string; referencePeriod: string; releaseAt: string; source: string; sourceUrl: string; status: string; updatedAt: string; }; type ImportantDatesGatewayResponse = { events?: GatewayImportantDate[]; refreshedAt?: string | null; }; export type ImportantDateEvent = { id: string; series: string; category: string; referencePeriod: string; releaseAt: string; source: string; sourceUrl: string; status: string; updatedAt: string; }; export type ImportantDatesSummary = { title: string; overallSummary: string; topEvents: Array<ImportantDateEvent & { reason: string }>; }; export type ImportantDatesRoundupResult = { ok: true; title: string; generatedAt: string; refreshedAt: string | null; daysAhead: number; limit: number; category?: string; series?: string[]; summary: ImportantDatesSummary; events: ImportantDateEvent[]; }; function resolveGatewayBase() { const baseUrl = process.env.OPENPOND_GATEWAY_URL; if (!baseUrl) { throw new Error("OPENPOND_GATEWAY_URL is required for Important Dates Roundup."); } return baseUrl.replace(/\/$/, ""); } function isRecord(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function normalizeImportantDate(value: unknown): ImportantDateEvent | null { if (!isRecord(value)) return null; const id = typeof value.id === "string" ? value.id : ""; const series = typeof value.series === "string" ? value.series : ""; const category = typeof value.category === "string" ? value.category : ""; const referencePeriod = typeof value.referencePeriod === "string" ? value.referencePeriod : ""; const releaseAt = typeof value.releaseAt === "string" ? value.releaseAt : ""; const source = typeof value.source === "string" ? value.source : ""; const sourceUrl = typeof value.sourceUrl === "string" ? value.sourceUrl : ""; const status = typeof value.status === "string" ? value.status : ""; const updatedAt = typeof value.updatedAt === "string" ? value.updatedAt : ""; if (!id || !series || !releaseAt) { return null; } return { id, series, category, referencePeriod, releaseAt, source, sourceUrl, status, updatedAt, }; } function dedupeEvents(events: ImportantDateEvent[]): ImportantDateEvent[] { const seen = new Set<string>(); const deduped: ImportantDateEvent[] = []; for (const event of events) { const key = `${event.id}:${event.releaseAt}`; if (seen.has(key)) continue; seen.add(key); deduped.push(event); } return deduped; } function buildFallbackSummary(params: { config: ImportantDatesRoundupConfig; events: ImportantDateEvent[]; }): ImportantDatesSummary { const topEvents = params.events.slice(0, params.config.summaryMaxEvents); if (topEvents.length === 0) { return { title: params.config.title, overallSummary: "", topEvents: [], }; } return { title: params.config.title, overallSummary: "", topEvents: topEvents.map((event) => ({ ...event, reason: "", })), }; } async function fetchImportantDates( config: ImportantDatesRoundupConfig, ): Promise<{ events: ImportantDateEvent[]; refreshedAt: string | null }> { const gatewayBase = resolveGatewayBase(); const requestLimit = Math.min(Math.max(config.limit * 3, config.limit), 50); const search = new URLSearchParams({ daysAhead: String(config.daysAhead), limit: String(requestLimit), }); if (config.category) { search.set("category", config.category); } const response = await fetch( `${gatewayBase}/v1/macro/important-dates?${search.toString()}`, { method: "GET", }, ); if (!response.ok) { throw new Error(`important-dates request failed (${response.status})`); } const payload = (await response.json().catch(() => null)) as | ImportantDatesGatewayResponse | null; const seriesFilters = new Set( (config.series ?? []).map((series) => series.trim().toLowerCase()), ); const events = dedupeEvents( (Array.isArray(payload?.events) ? payload.events : []) .map((event) => normalizeImportantDate(event)) .filter((event): event is ImportantDateEvent => event !== null) .filter((event) => { if (seriesFilters.size === 0) return true; return seriesFilters.has(event.series.trim().toLowerCase()); }), ) .sort((left, right) => { const leftTs = Date.parse(left.releaseAt); const rightTs = Date.parse(right.releaseAt); return leftTs - rightTs; }) .slice(0, config.limit); return { events, refreshedAt: typeof payload?.refreshedAt === "string" ? payload.refreshedAt : null, }; } async function summarizeImportantDates(params: { config: ImportantDatesRoundupConfig; events: ImportantDateEvent[]; }): Promise<ImportantDatesSummary> { return buildFallbackSummary(params); } export async function runImportantDatesRoundup( config: ImportantDatesRoundupConfig, ): Promise<ImportantDatesRoundupResult> { const generatedAt = new Date().toISOString(); const { events, refreshedAt } = await fetchImportantDates(config); const summary = await summarizeImportantDates({ config, events }); return { ok: true, title: config.title, generatedAt, refreshedAt, daysAhead: config.daysAhead, limit: config.limit, category: config.category, series: config.series, summary, events, }; }