SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.5 KB · 60 lines typescript
Raw Blame History
1import { eq, sql } from 'drizzle-orm';2import { crawlState } from '@rareindex/database';3import type { BudgetStore } from '@rareindex/connectors';4import { db } from './db.ts';56/**7 * Crawl budget engine (§170): per-URL content hash + change-interval estimate.8 * nextFetchAt = lastFetched + clamp(estimatedChangeInterval / 2, minH, maxH).9 * Unchanged fetches lengthen the interval (×1.5), changes shorten it (×0.5).10 */11export function createBudgetStore(opts: { connectorId: string; minHours?: number; maxHours?: number }): BudgetStore {12  const minH = opts.minHours ?? 1;13  const maxH = opts.maxHours ?? 24 * 14;14  return {15    async get(urlHash) {16      const [row] = await db().select({ contentHash: crawlState.contentHash, nextFetchAt: crawlState.nextFetchAt }).from(crawlState).where(eq(crawlState.urlHash, urlHash)).limit(1);17      return row ? { contentHash: row.contentHash, nextFetchAt: row.nextFetchAt } : null;18    },19    async record(input) {20      const now = new Date();21      const [prev] = await db().select().from(crawlState).where(eq(crawlState.urlHash, input.urlHash)).limit(1);22      let interval = prev?.changeIntervalHours ?? 24;23      if (prev) interval = input.changed ? Math.max(minH, interval * 0.5) : Math.min(maxH, interval * 1.5);24      const failures = input.status && input.status >= 400 ? (prev?.failures ?? 0) + 1 : 0;25      const backoff = failures > 0 ? Math.min(maxH, 2 ** failures) : interval / 2;26      const nextFetchAt = new Date(now.getTime() + Math.max(minH, backoff) * 3600_000);27      await db()28        .insert(crawlState)29        .values({30          urlHash: input.urlHash,31          connectorId: input.connectorId,32          url: input.url,33          contentHash: input.contentHash,34          lastFetchedAt: now,35          lastChangedAt: input.changed ? now : null,36          fetchCount: 1,37          changeCount: input.changed ? 1 : 0,38          changeIntervalHours: interval,39          nextFetchAt,40          lastStatus: input.status,41          failures,42        })43        .onConflictDoUpdate({44          target: crawlState.urlHash,45          set: {46            contentHash: input.contentHash,47            lastFetchedAt: now,48            lastChangedAt: input.changed ? now : sql`${crawlState.lastChangedAt}`,49            fetchCount: sql`${crawlState.fetchCount} + 1`,50            changeCount: sql`${crawlState.changeCount} + ${input.changed ? 1 : 0}`,51            changeIntervalHours: interval,52            nextFetchAt,53            lastStatus: input.status,54            failures,55          },56        });57    },58  };59}60