Klucz idempotencji dla płatności
Klient nie dostał odpowiedzi na „obciąż 100 zł” przez timeout i ponawia żądanie. Aby nie pobrać pieniędzy dwa razy, takie API przyjmują nagłówek Idempotency-Key. Napisz withIdempotency(handler, { ttlMs, now }), które zwraca (key, payload) => Promise.
- Pierwsze wywołanie z kluczem uruchamia handler(payload); równoległe wywołania z tym samym kluczem, gdy trwa, dostają tę samą obietnicę.
- Udany wynik jest pamiętany przez ttlMs i zwracany bez ponownego wywołania.
- Błąd nie jest pamiętany: następne wywołanie z kluczem spróbuje ponownie.
- Ten sam klucz z innym payload (porównanie przez JSON.stringify) jest odrzucany z Idempotency key reused with a different payload.
Ctrl+Enter uruchamia testy. Tab robi wcięcie; aby wyjść z edytora klawiaturą, naciśnij Esc, a potem Tab. Kod zapisuje się w tej przeglądarce.
Testy (6)
let t = 0; const charge = fn(async (p) => ({ id: 'ch_1', amount: p.amount })); const pay = withIdempotency(charge, { ttlMs: 60_000, now: () => t }); const first = await pay('key-1', { amount: 100 }); const second = await pay('key-1', { amount: 100 }); expect(second).toEqual(first); expect(charge).toHaveBeenCalledTimes(1);
let t = 0; const charge = fn(async (p) => { await wait(20); return p.amount; }); const pay = withIdempotency(charge, { ttlMs: 60_000, now: () => t }); const results = await Promise.all([pay('k', { amount: 5 }), pay('k', { amount: 5 }), pay('k', { amount: 5 })]); expect(results).toEqual([5, 5, 5]); expect(charge).toHaveBeenCalledTimes(1);
let t = 0; const pay = withIdempotency(async (p) => p.amount, { ttlMs: 60_000, now: () => t }); await pay('k', { amount: 100 }); await expect(pay('k', { amount: 999 })).rejects.toThrow('different payload');
let t = 0; let calls = 0; const pay = withIdempotency(async () => { calls++; if (calls === 1) throw new Error('gateway timeout'); return 'ok'; }, { ttlMs: 60_000, now: () => t }); await expect(pay('k', {})).rejects.toThrow('gateway timeout'); expect(await pay('k', {})).toBe('ok'); expect(calls).toBe(2);
let t = 0; const charge = fn(async () => 'ok'); const pay = withIdempotency(charge, { ttlMs: 1000, now: () => t }); await pay('k', {}); t = 999; await pay('k', {}); expect(charge).toHaveBeenCalledTimes(1); t = 1000; await pay('k', {}); expect(charge).toHaveBeenCalledTimes(2);
let t = 0; const charge = fn(async (p) => p.n); const pay = withIdempotency(charge, { ttlMs: 1000, now: () => t }); expect(await pay('a', { n: 1 })).toBe(1); expect(await pay('b', { n: 2 })).toBe(2); expect(charge).toHaveBeenCalledTimes(2);