import { sha256, type ConnectorHealth, type NormalizedRecord } from '@rareindex/shared'; import { healthFromRuns } from './health.js'; import { policyFor, type DomainPolicy } from './domains.js'; import type { ConnectorMeta, CrawlContext, HealthContext, RareIndexConnector, RawRecordInput, RawRecordLike } from './types.js'; /** * Convenience base class. Connectors may implement the interface directly; this class adds * rate limiting, checkpointing helpers and a default health check. */ export abstract class BaseConnector implements RareIndexConnector { abstract readonly version: string; abstract readonly parserVersion: string; readonly urlPatterns?: RegExp[]; /** minimum ms between requests to the source (politeness) */ protected minIntervalMs = 500; private lastRequestAt = 0; constructor(public readonly meta: ConnectorMeta) {} abstract crawl(ctx: CrawlContext): AsyncIterable; abstract normalize(raw: RawRecordLike): Promise; async healthCheck(ctx: HealthContext): Promise { const h = healthFromRuns(ctx, this.meta.schemaVersion); if (ctx.recentRuns.length === 0) { // Fresh connector: light probe of the source root. const res = await ctx.fetch(this.meta.sourceUrl, { engines: ['api'], failOnHttpError: false, responseType: 'text', minQuality: 0 }); h.status = res.httpStatus && res.httpStatus < 500 ? 'unknown' : 'failing'; h.last_error = res.error; } return h; } /** Politeness delay between requests: the larger of the connector's own interval and the domain policy (SPEC ยง16). */ protected async throttle(url?: string): Promise { const interval = Math.max(this.minIntervalMs, policyFor(url ?? this.meta.sourceUrl).minIntervalMs); const wait = this.lastRequestAt + interval - Date.now(); if (wait > 0) await new Promise((r) => setTimeout(r, wait)); this.lastRequestAt = Date.now(); } /** Effective per-domain policy for this connector's source. */ protected get policy(): DomainPolicy { return policyFor(this.meta.sourceUrl); } /** Stable content hash for dedupe of raw payloads. */ protected hash(payload: unknown): string { return sha256(typeof payload === 'string' ? payload : JSON.stringify(payload)); } /** Helper: yield-limit aware counter. */ protected reached(ctx: CrawlContext, count: number): boolean { return ctx.options.limit !== undefined && count >= ctx.options.limit; } }