idem-key
v0.1.0
Published
http idempotency keys done right. fingerprints the request, dedups in flight, replays the original response
Maintainers
Readme
idem-key
http idempotency keys done right. fingerprints the request, dedups in flight, replays the original response.
npm i idem-keywhy
"retry the payment" is the most dangerous line in any api client. the standard answer is an idempotency key, and almost every implementation gets one of these wrong:
- the same key with a different body runs anyway — a client bug becomes a double charge
- two concurrent retries both run — the check and the write are not atomic
- a failure gets cached — the retry replays an error forever
- only the body is stored — the replay loses the status code
use
import { Idempotency } from 'idem-key'
const idem = new Idempotency()
const out = await idem.run(key, { method: 'POST', path: '/charges', body }, async () => {
const charge = await stripe.charges.create(body)
return { status: 201, body: charge }
})
out.replayed // false the first time, true after
out.response // the same response either waysame key, different request
await idem.run('k1', { body: { amount: 100 } }, work)
await idem.run('k1', { body: { amount: 999999 } }, work)
// IdempotencyError: key k1 was already used with a different request (KEY_REUSED)the work does not run. that is the whole point — the key is a promise that this exact request happens once.
key order in the body does not count as a different request:
{ a: 1, b: 2 } === { b: 2, a: 1 } // same fingerprintconcurrent retries
const a = idem.run('k1', req, slowWork) // runs
const b = idem.run('k1', req, slowWork) // IdempotencyError: in progress (IN_PROGRESS)the gate is store.create(), which must only write when the key is absent and report whether it won. that is a single atomic operation — INSERT … ON CONFLICT DO NOTHING, SETNX, or a unique index. no read-then-write race.
failures are not results
await idem.run('k1', req, failing) // throws
await idem.run('k1', req, working) // runs again, not replayeda thrown error deletes the record. only a returned response is stored.
stuck records
if a process dies mid-request the record would block the key forever. after leaseMs (default 60s) the next caller takes it over.
express
import { middleware } from 'idem-key'
app.post('/charges', middleware({ required: true }), handler)reads Idempotency-Key, sets Idempotent-Replay: true on a replay, returns 409 for in-progress and 422 for a reused key.
your own store
const idem = new Idempotency({
store: {
async get (key) { … },
async create (record) { /* must be atomic, return false if it exists */ },
async update (record) { … },
async delete (key) { … }
}
})create is the only method that has to be atomic. everything else is ordinary.
api
| | |
|---|---|
| new Idempotency({ store?, ttlMs?, leaseMs?, now? }) | ttl 24h, lease 60s |
| .run(key, request, work) | { replayed, response } |
| middleware({ header?, required?, … }) | express style |
| fingerprint(request) | sha256 of method + path + canonical body |
| MemoryStore | for tests and single process |
zero dependencies. node crypto only. types included.
licence
MIT
