SPB Git

Repository Public

gecko.test.ts 3.4 KB 47 lines typescript

1// v10: the price feed helper. A refused request must not blank the prices for the whole TTL (the cause of « Prix désactivés »2// and of the missing daily change on a fresh stack), concurrent callers must share one request, and a stale value is served3// while the feed is down. Pure: a scripted fetch and a manual clock.4import { describe, expect, it } from 'vitest';5import { createGecko } from '../src/gecko.ts';67function feed(script: (number | unknown)[]) {8  const calls: string[] = [];9  const fetchJson = async (url: string) => { calls.push(url); const next = script.length > 1 ? script.shift() : script[0];10    return typeof next === 'number' ? { ok: false, status: next, json: async () => ({}) } : { ok: true, status: 200, json: async () => next }; };11  return { calls, fetchJson };12}1314describe('CoinGecko reads', () => {15  it('a refused request is retried once at once; the value is then cached for its TTL', async () => {16    let t = 1_000_000; const f = feed([429, [{ id: 'bitcoin' }]]), gecko = createGecko(f.fetchJson, { now: () => t, pauseMs: 1 });17    expect(await gecko('markets:usd', 240_000, '/coins/markets?x')).toEqual([{ id: 'bitcoin' }]);18    expect(f.calls).toHaveLength(2); expect(f.calls[0]).toBe('https://api.coingecko.com/api/v3/coins/markets?x');19    t += 239_000; await gecko('markets:usd', 240_000, '/coins/markets?x'); expect(f.calls).toHaveLength(2);20    t += 2_000; await gecko('markets:usd', 240_000, '/coins/markets?x'); expect(f.calls).toHaveLength(3);21  });22  it('a failure with no earlier value answers null for 15 s only — not for the TTL — then asks again', async () => {23    let t = 0; const f = feed([429, 429, 429, 429, [{ id: 'bitcoin' }]]), gecko = createGecko(f.fetchJson, { now: () => t, pauseMs: 1 });24    expect(await gecko('k', 240_000, '/p')).toBeNull(); expect(f.calls).toHaveLength(2);                // the call and its one retry25    t += 14_000; expect(await gecko('k', 240_000, '/p')).toBeNull(); expect(f.calls).toHaveLength(2);    // not hammered meanwhile26    t += 2_000; expect(await gecko('k', 240_000, '/p')).toBeNull(); expect(f.calls).toHaveLength(4);27    t += 16_000; expect(await gecko('k', 240_000, '/p')).toEqual([{ id: 'bitcoin' }]);                  // 32 s after the first refusal, not 240 s28  });29  it('the last good value is served while the feed is down', async () => {30    let t = 0; const f = feed([{ v: 1 }, 500, 500, 500, 500, { v: 2 }]), gecko = createGecko(f.fetchJson, { now: () => t, pauseMs: 1 });31    expect(await gecko('k', 60_000, '/p')).toEqual({ v: 1 });32    t += 61_000; expect(await gecko('k', 60_000, '/p')).toEqual({ v: 1 });33    t += 16_000; expect(await gecko('k', 60_000, '/p')).toEqual({ v: 1 });34    t += 16_000; expect(await gecko('k', 60_000, '/p')).toEqual({ v: 2 });35  });36  it('concurrent callers of one key share one request; different keys do not wait for each other', async () => {37    const f = feed([{ v: 1 }]), gecko = createGecko(f.fetchJson, { pauseMs: 1 });38    const [a, b, c] = await Promise.all([gecko('k', 1000, '/p'), gecko('k', 1000, '/p'), gecko('other', 1000, '/q')]);39    expect(a).toEqual({ v: 1 }); expect(b).toEqual({ v: 1 }); expect(c).toEqual({ v: 1 });40    expect(f.calls).toHaveLength(2);41  });42  it('a network error is a failure like another', async () => {43    const gecko = createGecko(async () => { throw new Error('ECONNRESET'); }, { pauseMs: 1 });44    expect(await gecko('k', 1000, '/p')).toBeNull();45  });46});47
Files main
Menu

Theme