npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@desolint/idempotency

v0.0.1

Published

Generic idempotency-key middleware for Express — pluggable storage, no Mongo/Redis dependency baked in. No root export — import only what you need: @desolint/idempotency/config, /services.

Readme

@desolint/idempotency

Generic idempotency-key middleware for Express. Detects duplicate requests (same user + method + path + params + query + body) and blocks retries of an in-flight or already-completed request.

No storage baked in. The package has zero dependency on Mongo, Redis, or any Desolint package — you supply a small storage adapter (get/ create/update) wrapping whatever database you already use.

No root export. import ... from '@desolint/idempotency' resolves to nothing — import only what you need:

@desolint/idempotency/config     @desolint/idempotency/services

| Subpath | What it's for | | ----------- | ---------------------------------------------------------- | | /config | initializeIdempotency — wire up your store + hooks once. | | /services | idempotencyMiddleware() — the actual Express middleware. |


Requirements

  • Node.js 22 or newer (declared in engines)
  • npm 7 or newer — npm 7+ installs peer dependencies automatically

Install

npm install @desolint/idempotency

That is all you need on npm 7+: express is listed as a peer dependency, so npm resolves and installs it for you — and in practice you already have it.

Neither installs peer dependencies automatically, so name it explicitly:

yarn add @desolint/idempotency express
# or
pnpm add @desolint/idempotency express

Why express is a peer dependency, not a regular one

This package ships middleware, which only works when mounted on the same express instance your application already created. A second copy would carry its own router and request/response prototypes, so the middleware would either fail to mount or run against objects your app never sees.

Declaring it as a peer means npm reuses the copy your application already has instead of nesting a second one. You keep control of the version; this package just states the range it works with.

Storage is deliberately not a dependency: you pass your own IdempotencyStore, so nothing here pulls in Mongo, Redis, or any other database driver.


Quick start

// config/idempotency.ts
import { initializeIdempotency } from "@desolint/idempotency/config";
import IdempotencyModel from "@/models/IdempotencyModel";
import GeneralServices from "@/services/generalServices";
import { IdempotencyErrorsFactories } from "@/factories";

initializeIdempotency({
  // The storage adapter — three functions wrapping your existing Mongo
  // model. Swap this for a Redis adapter, an in-memory Map, whatever —
  // the middleware doesn't care.
  store: {
    get: async ({ key }) => {
      const { doc } = await GeneralServices.findOne({
        model: IdempotencyModel,
        query: { key },
      });
      return doc ? { status: (doc as { status: string }).status } : null;
    },
    create: async ({ key }) => {
      try {
        await GeneralServices.create({
          model: IdempotencyModel,
          data: { key, status: "processing" },
        });
        return { created: true };
      } catch (err) {
        if ((err as { code?: number }).code === 11000)
          return { created: false };
        throw err;
      }
    },
    update: async ({ key, status, ifStatus }) => {
      // `ifStatus` makes the failed-key retry path race-safe: only apply
      // the update if the key is still in that status. Fold it into the
      // query when present, and report whether it actually matched.
      const { doc } = await GeneralServices.findOneAndUpdate({
        model: IdempotencyModel,
        query: ifStatus ? { key, status: ifStatus } : { key },
        data: { status },
      });
      return { updated: Boolean(doc) };
    },
  },

  // Who the request belongs to — scopes the idempotency key per-user.
  getIdentity: ({ req }) => req.extra.jwtToken!.user._id,

  // Called when a duplicate is detected — throw your app's own error.
  onDuplicate: () => {
    throw IdempotencyErrorsFactories.idempotencyKeyAlreadyProcessing();
  },

  // Called when the *store itself* errors (DB down, etc.) — optional.
  onError: (error) => console.error("Idempotency store error", { error }),
});
// app.ts
import { idempotencyMiddleware } from "@desolint/idempotency/services";

app.use(idempotencyMiddleware());

/config

initializeIdempotency({store, getIdentity, onDuplicate, onError?, failOpen?})

| Param | Type | Required | Notes | | ------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | store | IdempotencyStore | yes | {get, create, update} — see Writing a store below. | | getIdentity | ({req}) => string | yes | Whatever identifies the caller (user id, API key, ...). Scopes the key so different callers never collide. | | onDuplicate | ({req, res}) => void | yes | Runs instead of next() for a duplicate. Throw your own error or write to res directly — whatever your app already does. | | onError | (error: Error) => void | no | Fires when the store errors (not for a duplicate — that's onDuplicate). | | failOpen | boolean | no | Default false: a store error re-throws (request fails, fail-closed). Set true to let the request through unprotected instead. |

IdempotencyStore

interface IdempotencyStore {
  get: (params: {
    key: string;
  }) => Promise<{ status: IdempotencyStatus } | null>;
  create: (params: { key: string }) => Promise<{ created: boolean }>;
  update: (params: {
    key: string;
    status: IdempotencyStatus;
    ifStatus?: IdempotencyStatus;
  }) => Promise<{ updated: boolean } | void>;
}
  • getnull means "never seen this key" (fresh request).
  • create — must be atomic against your backend: created: false means a concurrent request already claimed the key first (e.g. a Mongo unique-index violation, or Redis SET key val NX returning nil).
  • update — changes a key's status (processingcompleted/failed, or failedprocessing on retry). When ifStatus is passed, apply the update only if the key's current status still matches it, and return {updated: false} when it didn't — this is what makes reclaiming a failed key for retry safe against two concurrent retries of the same key. Optional to implement: returning void (or ignoring ifStatus) keeps working exactly as before, just without that race protection.

IDEMPOTENCY_STATUSES (processing/completed/failed) is exported from /config too, so your store implementation can reference the same constants instead of hardcoding strings.


IdempotencyConfig

The object initializeIdempotency takes. Exported so you can type your own wiring module against it.

IDEMPOTENCY_STATUSES / IdempotencyStatus

The three states a key can be in — processing, completed, failed. Your store receives and returns these; use the constant rather than string literals so a typo is a compile error.

resetIdempotencyConfig()

Test-only escape hatch. Clears the configuration so each test file starts from a clean slate instead of inheriting the previous one's — the config lives on globalThis, so it would otherwise leak across files. Every package in this scope exposes the same hatch.

/services

idempotencyMiddleware()

Returns an Express middleware. On each request:

  1. Computes a key from getIdentity({req}) + method + path + params + query + body (sorted, so key order in the body never changes the key).
  2. Looks it up via store.get.
    • Not foundstore.create. If that loses a race (created: false), it's a duplicate.
    • Found, status failed → reclaimed for immediate retry.
    • Found, status processing/completed → duplicate.
  3. Duplicate → calls onDuplicate({req, res}) instead of next().
  4. Otherwise → calls next(), and once the response finishes, marks the key completed (2xx) or failed (anything else) via store.update.

A store failure at any point routes through onError/failOpen as described above — it never crashes the process.


generateIdempotencyKey({identity, method, path, params?, query?, body?})

Returns the SHA-256 key the middleware computes for a request. A pure function — no req, no Express types — exported so you can reproduce the same key outside the middleware, for example to look up one request's status from a support script.

Object keys are sorted recursively before hashing, including inside arrays, so two structurally identical payloads that differ only in key order ({a:1,b:2} vs {b:2,a:1}) produce the same key rather than two different ones.

Development

npm install     # install dependencies
npm run build   # type-check, then bundle each subpath into dist/
npm test        # jest
npm run lint    # eslint

scripts/build.mjs bundles each subpath into one self-contained JS file plus a .d.ts, then deletes everything else from dist/ — internal modules (src/idempotency/*, src/shared/*) never ship, so there is nothing for an editor or a moduleResolution: "node" consumer to resolve beyond the two public subpaths documented above.


License

MIT © Desolint — see LICENSE.

Free to use, modify and redistribute, commercially or otherwise. Provided "as is", without warranty or liability of any kind.