x402-toolpick / tree / src/index-do.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// IndexStore: SQLite Durable Object holding the merged x402 resource index as JSON chunks (one row per 1,000
// resources, ~37 rows per refresh; no KV). Searches run inside the object against an in-memory copy.
// Refresh is lazy (no cron): a query that finds the index older than REFRESH_AFTER_MS sets an alarm, and the alarm
// pulls the public discovery APIs. A failed refresh keeps the last good index.
import { DurableObject } from 'cloudflare:workers';
import { buildIndex } from './engine.js';
import { queryIndex } from './query.js';

export const REFRESH_AFTER_MS = 6 * 3600_000;
const CHUNK = 1000;

export class IndexStore extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;
    this.sql.exec('CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)');
    this.sql.exec('CREATE TABLE IF NOT EXISTS chunks (gen INTEGER NOT NULL, i INTEGER NOT NULL, data TEXT NOT NULL, PRIMARY KEY (gen, i))');
    this.mem = null;
  }
  meta(k) { const r = this.sql.exec('SELECT v FROM meta WHERE k = ?', k).toArray()[0]; return r ? JSON.parse(r.v) : null; }
  setMeta(k, v) { this.sql.exec('INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v', k, JSON.stringify(v)); }
  load() {
    const info = this.meta('index');
    if (!info) return null;
    if (this.mem && this.mem.gen === info.gen) return this.mem;
    const rows = [];
    for (const r of this.sql.exec('SELECT data FROM chunks WHERE gen = ? ORDER BY i', info.gen)) rows.push(...JSON.parse(r.data));
    this.mem = { gen: info.gen, info, rows };
    return this.mem;
  }
  async maybeRefresh(now) {
    const info = this.meta('index');
    const last = this.meta('last_attempt') || 0;
    if (info && now - info.at < REFRESH_AFTER_MS) return false;
    if (now - last < 10 * 60_000) return false; // at most one attempt per 10 minutes
    const alarm = await this.ctx.storage.getAlarm();
    if (alarm) return false;
    await this.ctx.storage.setAlarm(now + 100);
    return true;
  }
  async alarm() {
    const now = Date.now();
    this.setMeta('last_attempt', now);
    this.mem = null; // free the in-memory copy while building (memory headroom); reloaded from SQLite on the next query
    try {
      const idx = await buildIndex({ fetchImpl: (...a) => fetch(...a), now });
      const prev = this.meta('index');
      const gen = (prev?.gen || 0) + 1;
      for (let i = 0; i * CHUNK < idx.rows.length; i++) this.sql.exec('INSERT OR REPLACE INTO chunks (gen, i, data) VALUES (?, ?, ?)', gen, i, JSON.stringify(idx.rows.slice(i * CHUNK, (i + 1) * CHUNK)));
      this.setMeta('index', { gen, at: idx.at, counts: idx.counts, sources_failed: idx.sources_failed });
      this.sql.exec('DELETE FROM chunks WHERE gen <> ?', gen);
      this.setMeta('last_error', null);
      this.mem = null;
    } catch (e) {
      this.setMeta('last_error', { at: now, error: String(e?.message || e).slice(0, 200) });
      console.warn(`toolpick refresh failed: ${String(e?.message || e).slice(0, 200)}`);
    }
  }
  /** RPC: run one query against the index. Returns { info, result } or { info: null }. */
  async query(tier, v, nowMs) {
    const now = nowMs || Date.now();
    await this.maybeRefresh(now);
    const m = this.load();
    if (!m) return { info: null, last_error: this.meta('last_error') };
    return { info: m.info, result: queryIndex(m.rows, tier, v, Math.floor(now / 1000)) };
  }
  async status() { const i = this.meta('index'); return { index: i, last_error: this.meta('last_error'), last_attempt: this.meta('last_attempt') }; }
}

agentgate-source: code mirror on Cloudflare Pages. All repos