Загрузка…
Загрузка…
HTTP / REST · middle · сложность 6
Клиент шлёт стабильный Idempotency-Key на логическое действие; сервер lock → process once → replay. Параллельные дубликаты coalesce в один promise.
// CLIENT: send a stable key per logical action; reuse it across retries.
async function pay(cartId, amount) {
const key = crypto.randomUUID(); // once, BEFORE any attempt
return fetchRetry('/charge', {
method: 'POST',
headers: { 'Idempotency-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({ cartId, amount }),
}); // every retry inside fetchRetry reuses `key` → server dedupes
}// CLIENT dedupe: coalesce concurrent identical requests into one promise.
const inflight = new Map();
function dedupe(key, fn) {
if (inflight.has(key)) return inflight.get(key); // reuse the in-flight call
const p = fn().finally(() => inflight.delete(key)); // clear when settled
inflight.set(key, p);
return p;
}
// Double-click → ONE network request, both callers await the same promise.
btn.onclick = () => dedupe('checkout', () => pay(cartId, amount));// SERVER sketch: lock, process once, replay thereafter.
async function handleCharge(req, res) {
const key = req.headers['idempotency-key'];
if (!key) return res.status(400).json({ error: 'Idempotency-Key required' });
const existing = await store.get(key);
if (existing?.status === 'done') return res.status(200).json(existing.response); // replay
if (existing?.status === 'locked') return res.status(409).json({ error: 'in progress' });
await store.set(key, { status: 'locked' }); // claim the key
const receipt = await chargeOnce(req.body); // the real, unsafe work
await store.set(key, { status: 'done', response: receipt });
res.status(201).json(receipt);
}Key создаётся один раз per action; сервер хранит outcome ~24h и replay'ит; client dedupe закрывает double-click, idempotency key — lost response на retry.