slice 1a-1d: DB + CacheRepository + YFinance adapter + AdapterQueue
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.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user