1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// lib/refresh.ts — fetch + cache orchestration for sources.
// A source's latest Snapshot is the cache. We refresh when forced or stale.
import type { SourceRow, SnapshotRow } from "@/db/schema";
import type { SourceKind } from "@/lib/schemas/source";
import { getSourceModule, isSourceConfigured } from "@/lib/sources/registry";
import { insertSnapshot, latestSnapshot } from "@/lib/store";
const FETCH_TIMEOUT_MS = 15_000;
export interface SourceState {
source: SourceRow;
hasModule: boolean;
configured: boolean;
snapshot: SnapshotRow | null;
}
function isStale(snapshot: SnapshotRow | null, refreshSeconds: number): boolean {
if (!snapshot) return true;
const age = (Date.now() - new Date(snapshot.fetchedAt).getTime()) / 1000;
return age >= refreshSeconds;
}
// Run the source's fetcher once and store the result as a snapshot.
export async function refreshSource(source: SourceRow): Promise<SnapshotRow> {
const mod = getSourceModule(source.kind as SourceKind);
if (!mod) {
return insertSnapshot({
sourceId: source.id,
payload: null,
ok: false,
error: `No fetcher for "${source.kind}"`,
});
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const config = mod.configSchema.parse(source.config ?? {});
const payload = await mod.fetch({ config, env: process.env, signal: controller.signal });
const validated = mod.payloadSchema.parse(payload);
return insertSnapshot({ sourceId: source.id, payload: validated, ok: true, error: null });
} catch (err) {
const message = err instanceof Error ? err.message : "Fetch failed";
return insertSnapshot({ sourceId: source.id, payload: null, ok: false, error: message });
} finally {
clearTimeout(timer);
}
}
// Cached state only — no network. Used for instant dashboard paint.
export function currentState(source: SourceRow): SourceState {
return {
source,
hasModule: Boolean(getSourceModule(source.kind as SourceKind)),
configured: isSourceConfigured(source.kind as SourceKind, source.config),
snapshot: latestSnapshot(source.id) ?? null,
};
}
export function isSourceStale(source: SourceRow): boolean {
return isStale(latestSnapshot(source.id) ?? null, source.refreshSeconds);
}
// Return current state, refreshing first when forced or stale (and refreshable).
export async function ensureFresh(
source: SourceRow,
opts: { force?: boolean } = {},
): Promise<SourceState> {
const mod = getSourceModule(source.kind as SourceKind);
const hasModule = Boolean(mod);
const configured = isSourceConfigured(source.kind as SourceKind, source.config);
let snapshot = latestSnapshot(source.id) ?? null;
const refreshable = hasModule && configured && source.enabled;
if (refreshable && (opts.force || isStale(snapshot, source.refreshSeconds))) {
snapshot = await refreshSource(source);
}
return { source, hasModule, configured, snapshot };
}
|