Files
investor-flow/app/server/src/adapters/SourceAdapter.ts
T

43 lines
2.2 KiB
TypeScript
Raw Normal View History

// 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<FetchResult>;
}
/**
* 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<CacheKey, FetchResult>();
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<FetchResult> {
this.calls.push(key);
const r = this.canned.get(key);
if (!r) throw new Error(`FakeSourceAdapter: no canned response for ${key}`);
return r;
}
}