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

@sebspark/idempotency

v1.0.0

Published

Idempotency guard for multi-step async processes, backed by Redis. Prevents duplicate execution when the same operation is triggered more than once concurrently.

Readme

@sebspark/idempotency

Idempotency guard for multi-step async processes, backed by Redis. Prevents duplicate execution when the same operation is triggered more than once concurrently.

How it works

When a guard is created it immediately fires two parallel checks against Redis — without blocking the process from starting:

  • GET on the key: if it already exists, a previous run completed successfully.
  • SUBSCRIBE on a channel: if another in-flight process calls end() first, this one is notified.

Either check triggers an abort signal. If the guard's run() is still in progress when the signal fires, it rejects with an IdempotencyConflictError containing the value stored by the winning process.

The process that calls end(value) first stores the key in Redis and publishes to the channel, causing any concurrent processes to abort.

Process A ──── create ──── run(end => { ...; end(v) }) ✓   (stores + publishes)
Process B ──── create ──── run(end => { ... }) ──✗          (aborted by A's publish)

Installation

yarn add @sebspark/idempotency

Requires a Redis client from the redis package:

yarn add redis

Usage

import { createClient } from 'redis'
import { IdempotencyGuard, IdempotencyConflictError } from '@sebspark/idempotency'

const client = createClient({ url: 'redis://localhost:6379' })
await client.connect()

Basic example

run() receives an end callback as its first argument. Call end(value) once all steps have succeeded and before the function returns. The value passed to end is what gets stored in Redis and broadcast to concurrent guards — it does not have to match the function's return value.

const guard = await IdempotencyGuard.create<Order, OrderId>(`order:${idempotencyKey}`, client)

try {
  const order = await guard.run(async (end) => {
    const validated = await validateOrder(body)
    const reserved  = await reserveInventory(validated)
    const charged   = await chargePayment(reserved)
    end(charged.id)   // commit the idempotency value — no await needed
    return charged    // return value may differ from the committed value
  })

  return res.status(201).location(`/orders/${order.id}`).send()

} catch (err) {
  if (err instanceof IdempotencyConflictError) {
    // err.value is the OrderId passed to end() by the winning process
    return res.status(200).location(`/orders/${err.value}`).send()
  }
  throw err
}

The two status codes reflect whether this process did the work:

  • 201 Created — this process completed all steps and created the order.
  • 200 OK — another process already completed it. The outcome is identical, so respond with the same Location header. The client doesn't need to know which happened.

Type parameters

IdempotencyGuard<R, V> has two type parameters:

| Parameter | Description | Default | |-----------|-------------|---------| | R | Return type of the function passed to run() | — | | V | Type of the value passed to end() and carried by IdempotencyConflictError | R |

When the committed value and the return value are the same type, create<Order>() is sufficient.

Lifecycle

| Step | Description | |------|-------------| | IdempotencyGuard.create(key, client, options?) | Creates the guard and starts background Redis checks. | | guard.run(fn) | Passes an end callback to fn and races it against the abort signal. Resolves with the return value of fn. Rejects with IdempotencyConflictError if aborted, or IdempotencyEndNotCalledError if fn returns without calling end. | | end(value) | Stores value in Redis and publishes it to abort concurrent guards. Must be called exactly once before fn returns. Fire-and-forget — no need to await. |

Errors

| Error | When | |-------|------| | IdempotencyConflictError<V> | Another guard called end() first, or the key already exists from a previous run. err.value is the winning value. | | IdempotencyEndNotCalledError | fn returned without calling end. | | IdempotencyEndNotCalledError (different message) | end was called more than once. |

Options

IdempotencyGuard.create(key, client, {
  ttlSeconds: 86400 // How long the key lives in Redis. Default: 86400 (24h)
})

Using the signal directly

guard.signal is a standard AbortSignal. Pass it to outgoing requests to cancel them when the guard aborts:

const guard = await IdempotencyGuard.create<Order>(key, client)

const order = await guard.run(async (end) => {
  const data = await fetch('/external-api', { signal: guard.signal })
  const result = await processData(data)
  end(result)
  return result
})

Error handling

catch (err) {
  if (err instanceof IdempotencyConflictError) {
    console.log(err.value) // The value passed to end() by the winning process
  }
}

Concurrency model

Neither process waits for the initial Redis GET before starting its steps. This means two processes can run in parallel for a short window before one of them is aborted. The guarantee is:

  • At most one process calls end() — the first to call it wins.
  • Any concurrent process is aborted either by the GET response (key already exists) or by the SUBSCRIBE notification (another process just called end()).
  • The conflict error carries the result — no second Redis lookup needed in the caller.

This design optimises for the common case where duplicates are rare. Both processes do useful work up until the conflict is detected, rather than serialising on a lock upfront.

end() is fire-and-forget. Not awaiting it introduces a small race window: if the process crashes between calling end() and the write completing, the key is never stored and concurrent guards are never notified. This is an accepted tradeoff for throughput — the guarantee is eventual, not atomic.