x402-smartperps / tree / src/store.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// Snapshot store adapter: wraps the SnapStore Durable Object (production) or an in-memory object (tests), and runs
// refreshes (cron every 5 min; a lazy refresh is kicked in the background when a read finds the snapshot missing/old).
import { buildSnapshot } from './engine.js';
import { withDeadline } from './deadline.js';

export const LAZY_REFRESH_AFTER_MS = 8 * 60_000;

/** Refresh once under the store lock. Returns a short status object (never throws). */
export async function refresh(stub, { fetchImpl = globalThis.fetch, now = Date.now() } = {}) {
  try {
    if (!(await stub.lock(now))) return { ok: false, skipped: 'locked' };
    try {
      const prev = await stub.cohort();
      const { snapshot, cohortRecord } = await buildSnapshot({ fetchImpl, now, cohortRecord: prev });
      await stub.put(snapshot, cohortRecord !== prev ? cohortRecord : null);
      return { ok: true, covered: snapshot.covered, size: snapshot.cohort_size, coins: Object.keys(snapshot.coins).length, failed: snapshot.failed };
    } catch (e) { try { await stub.unlock(); } catch { /* lock expires */ } return { ok: false, error: String(e?.message || e).slice(0, 160) }; }
  } catch (e) { return { ok: false, error: String(e?.message || e).slice(0, 160) }; }
}

/** Store used by the app: read() with a 20 s per-isolate cache; a stale/missing snapshot kicks a background refresh. */
// `stub` may be a function returning a fresh stub: Durable Object stubs are bound to the request that created them, so the
// Worker passes () => env.SNAP.get(id) and a new stub is made per read (reusing one across requests throws cross-request I/O).
export function makeStore(stubOrFn, { fetchImpl = globalThis.fetch, now = () => Date.now(), readMs = 4000 } = {}) {
  let cached = null; let at = 0; let kicked = 0;
  return {
    async read(waitUntil) {
      const t = now();
      if (cached && t - at < 20_000) return cached;
      const stub = typeof stubOrFn === 'function' ? stubOrFn() : stubOrFn;
      const r = await withDeadline(stub.read(), readMs, 'snapshot store');
      cached = r; at = t;
      const age = r.snapshot ? t - r.snapshot.at : Infinity;
      if (age > LAZY_REFRESH_AFTER_MS && t - kicked > 60_000 && waitUntil) { kicked = t; try { waitUntil(refresh(stub, { fetchImpl, now: t })); } catch { /* not on Workers */ } }
      return r;
    },
    async cohort() { try { return (await this.read()).cohort; } catch { return null; } },
  };
}

/** In-memory stub with the SnapStore interface (tests, local node server). */
export function memoryStub() {
  const s = { snapshot: null, cohortRecord: null, hist: [], lockAt: 0 };
  return {
    state: s,
    async lock(now) { if (now - s.lockAt < 120_000) return false; s.lockAt = now; return true; },
    async unlock() { s.lockAt = 0; return true; },
    async cohort() { return s.cohortRecord; },
    async put(snapshot, cohortRecord) {
      s.snapshot = snapshot; if (cohortRecord) s.cohortRecord = cohortRecord; s.lockAt = 0;
      s.hist.push({ at: snapshot.at, coins: Object.fromEntries(Object.entries(snapshot.coins).map(([c, a]) => [c, { net: a.net, ln: a.ln, sn: a.sn, tl: a.tl, ts: a.ts }])) });
      return true;
    },
    async read() { return { snapshot: s.snapshot, cohort: s.cohortRecord?.cohort || [], hist: s.hist.filter((h) => !s.snapshot || h.at < s.snapshot.at) }; },
  };
}

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