// Investor Flow — SourceAdapter contract (DESIGN.md §3a Module 2 + §5). // // Implementation split (vs the design's single SourceAdapter interface): // - SourceFetch (here): the per-source fetch+parse contract. One concrete adapter per source. // - AdapterQueue (slice 1d): the shared queue/dedupe/backoff/drain orchestrator that calls // SourceFetch.fetchOne during drain and writes results to CacheRepository. // This preserves the design's intent (one seam per source; shared dedupe) while keeping each // micro-dispatch focused. The SPA never calls either directly — only CacheRepository.get → // AdapterQueue.queue → (drain) → SourceFetch.fetchOne → CacheRepository.set. import type { CacheKey, SourceKind, TtlClass, Provenance } from '../cache/CacheRepository.ts'; export interface FetchOpts { force?: boolean; } // force = bypass dedupe (admin.resetBackoff) export interface AdapterHealth { queued: number; in_flight: number; backoff_until?: string | null; last_error?: string; } export interface FetchResult { value: unknown; ttlClass: TtlClass; provenance: Provenance; } /** Per-source fetch+parse. The AdapterQueue calls fetchOne(key) during drain. */ export interface SourceFetch { readonly sourceKind: SourceKind; fetchOne(key: CacheKey): Promise; } /** * Test double: deterministic canned responses per key. Implements SourceFetch. * `calls` records every fetchOne invocation — used by dedupe tests * ("two concurrent snapshot calls collapse into one fetch" => calls.length === 1). */ export class FakeSourceAdapter implements SourceFetch { readonly sourceKind: SourceKind; private readonly canned = new Map(); public calls: CacheKey[] = []; constructor(sourceKind: SourceKind = 'yfinance') { this.sourceKind = sourceKind; } set(key: CacheKey, value: unknown, ttlClass: TtlClass, provenance?: Provenance): this { this.canned.set(key, { value, ttlClass, provenance: provenance ?? { fetchedAt: new Date().toISOString(), sourceKind: this.sourceKind } }); return this; } async fetchOne(key: CacheKey): Promise { this.calls.push(key); const r = this.canned.get(key); if (!r) throw new Error(`FakeSourceAdapter: no canned response for ${key}`); return r; } }