Node 26 + node:sqlite backend (zero native deps; runtime glue adapted from Bun-spec design, deep-module architecture unchanged). 29 tests green across schema/CacheRepository/YFinance-parse/AdapterQueue-dedupe.
43 lines
2.2 KiB
TypeScript
43 lines
2.2 KiB
TypeScript
// 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;
|
|
}
|
|
}
|