Skip to content

GustavoQnt/idempotency-kit

Repository files navigation

idempotency-kit

npm version npm downloads CI license

Traffic Dashboard

GitHub traffic snapshot (rolling window reported by GitHub API).

Totals since 2026-02-12: 141 views and 655 clones.

Date Views Clones
2026-03-17 0 13
2026-03-16 0 11
2026-03-15 1 14
2026-03-14 0 16
2026-03-13 0 14
2026-03-12 2 12
2026-03-11 0 15
2026-03-10 0 8
2026-03-09 2 10
2026-03-08 0 10
2026-03-07 0 11
2026-03-06 0 12
2026-03-05 0 13
2026-03-04 17 5
2026-03-03 0 12
2026-03-02 0 13
2026-03-01 0 14
2026-02-28 1 11
2026-02-27 1 14
2026-02-26 0 21
2026-02-25 0 18
2026-02-24 3 11
2026-02-23 0 13
2026-02-22 4 12
2026-02-21 6 13
2026-02-20 0 13
2026-02-19 3 15
2026-02-18 18 35
2026-02-17 5 12
2026-02-16 3 17
2026-02-15 17 34
2026-02-14 20 30
2026-02-13 38 183
2026-02-12 0 0

Updated automatically: 2026-03-19T05:03:59.163Z

Local-only idempotency for Node.js/TypeScript.

  • Dedup concurrent executions by key (single in-flight promise).
  • Return the same result on retries while TTL is valid.
  • Optional failed-result memoization (cacheFailures).
  • Abort one caller without canceling shared execution.
  • Zero infra (in-memory only).

Why this library

  • Webhook duplicates.
  • Client retries.
  • Double-submit and double-click actions.
  • Accidental job reprocessing.

Install

npm install idempotency-kit

Quick Start

import { IdempotencyLocal } from "idempotency-kit";

const idem = new IdempotencyLocal({ ttlMs: 30_000 });

const charge = await idem.run("charge:order-123", async () => {
  return { ok: true, chargeId: "ch_1" };
});

API

new IdempotencyLocal(options?)
createIdempotencyLocal(options?)

Main methods

Method Return Description
run(key, fn, options?) Promise<T> Executes once per key and memoizes result by TTL.
runWithMeta(key, fn, options?) Promise<{ value: T; meta: RunMeta }> Same as run, with resolution metadata.
delete(key) boolean Deletes one cached result key.
clear() void Clears all cached results.
prune() number Removes expired entries and returns removed count.
getStats() IdempotencyStats Read runtime counters and sizes.
dispose() void Disposes internal cache resources.
size number Current result-cache size.

Constructor options

Option Type Default Description
ttlMs number 30000 Success TTL in milliseconds.
cacheFailures boolean false Cache failed results for dedup/retry behavior.
failureTtlMs number ttlMs TTL for cached failures.
maxSize number undefined Max result entries (LRU evicts old entries).
cleanupIntervalMs number \| false false Periodic cleanup interval.
keyPrefix string "" Prefix applied as prefix:key.

Per-run options

Option Type Description
ttlMs number Override success TTL for this call.
cacheFailures boolean Override failure caching behavior for this call.
failureTtlMs number Override failure TTL for this call.
signal AbortSignal Cancel only this caller wait.

runWithMeta statuses

  • hit_completed
  • hit_failed
  • inflight_hit
  • miss_executed
  • miss_executed_failed

Behavior and semantics

run(key, fn) rules:

  1. If completed result is valid, returns it (hit_completed).
  2. If failed result is valid and cacheFailures=true, throws same error (hit_failed).
  3. If key is in-flight, waits on the same promise (inflight_hit).
  4. Otherwise executes fn:
    • success: caches completed result (miss_executed)
    • error: caches failed result only if enabled (miss_executed_failed)

Key normalization:

const finalKey = keyPrefix ? `${keyPrefix}:${key}` : key;

Abort behavior

import { AbortError, IdempotencyLocal, isAbortError } from "idempotency-kit";

const idem = new IdempotencyLocal();
const controller = new AbortController();

const promise = idem.run("job:1", doWork, { signal: controller.signal });
controller.abort();

try {
  await promise;
} catch (error) {
  if (isAbortError(error)) {
    // this caller stopped waiting
  }
}

Aborting one caller does not cancel the shared loader for other callers.

Errors

  • AbortError: thrown when caller signal is aborted.
  • isAbortError(error): type-safe helper.

Stats

getStats() returns:

  • runs
  • hitsCompleted
  • hitsFailed
  • inflightHits
  • missesExecuted
  • missesExecutedFailed
  • abortedWaits
  • size
  • inFlight

Benchmarks

Run:

npm run bench

Included scenarios:

  • Miss path (run with unique key).
  • Hit path (run with warm key).
  • 100 concurrent calls same key (dedup).
  • LRU pressure under maxSize.
  • Expiration cleanup with prune().

See BENCHMARK.md for details.

Latest local run

Date: February 13, 2026
Environment: Windows + Node v22.19.0 + Vitest 3.2.4

Scenario Throughput (hz)
run miss (new key each call) 672,647.73
run hit (same key) 707,083.15
inflight dedup (100 concurrent same key) 120.37
lru pressure (maxSize=1_000, 2_000 inserts) 544.62
prune expired (5_000 keys) 40.83

Quality checks

npm run typecheck
npm test
npm run build

Gotchas

  • Cache is per process; no cross-instance guarantees.
  • undefined return values are cached normally.
  • Default cacheFailures is false.

License

MIT (LICENSE)

About

A lightweight, local-first idempotency manager for TypeScript. Prevent duplicate operations with built-in request locking, status tracking, and seamless integration with @gustavoqnt/ttl-cache.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors