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

@nodii/idempotency

v0.10.0

Published

Idempotency library for the Nodii microservice stack — wrapForSagaStep + wrapForWebhook + wrapConsumerWorker + computeIdempotencyKey + ioredis CLAIM via Lua + sweeper + gRPC idempotencyGuard interceptor. Polyglot ship: TS + Python + Go in parity. Spec: pl

Readme

@nodii/idempotency

Spec: https://planning.dev.nucleus-cloud.in/api/v1/feature-docs?serviceId=nodii-libs&docKey=idempotency

Entry points

| Import | What it gives you | |---|---| | @nodii/idempotency | initIdempotency, the three wrappers (wrapForSagaStep / wrapForWebhook / wrapConsumerWorker), key helpers, the D242 outcome classifier, the sweeper | | @nodii/idempotency/grpc | idempotencyGuard — the server-side unary interceptor | | @nodii/idempotency/hono | idempotencyHono — the HTTP middleware (below) | | @nodii/idempotency/lease | SingleActiveLease (below) | | @nodii/idempotency/durable | Postgres consumer-dedup (below) | | @nodii/idempotency/test-doubles | InMemoryRedisClient / FailingRedisClient |

Boot

await initIdempotency({ redis, serviceId: "billing" });

That is the whole bootstrap. Since 0.10.0 it also arms the in-flight sweeper, so a handler that dies between CLAIM and COMPLETE no longer wedges its key for the full TTL — every retry used to 409 for 24h. Opt out with sweeper: false; tune with sweeper: { intervalMs, batchSize, … }. Call shutdownIdempotency() on SIGTERM to stop it and release its lease.

Two things about the sweep loop are worth knowing. Each process picks its own cadence within ±20% of intervalMs (intervalJitterRatio, set 0 for an exact interval) — thirteen services on one shared Redis must not all SCAN on the same 5-minute boundary. And the SCAN cursor persists across passes: a pass that hits maxBatchesPerPass stops, fires onTruncated and resumes from where it stopped. It used to restart at 0 every pass, so once the keyspace outgrew the batch cap the tail was never swept at all — silently, forever. Note that SCAN's COUNT is a hint over the whole Redis DB, not over keys matching the pattern, so on a shared instance that cap is nearer than it looks.

Also since 0.10.0: a throw carrying no numeric gRPC code classifies as transport (the key is RELEASED, the retry re-executes) rather than application (cached rejection). The D242 status table for CODED errors is unchanged. Pass defaultClass: "application" to restore the old fall-through.

HTTP idempotency (@nodii/idempotency/hono)

import { idempotencyHono } from "@nodii/idempotency/hono";
app.use("*", idempotencyHono());

Mount it BELOW your auth middleware. It needs the trusted, post-auth tenant and actor and it fails the request closed without them.

A mutating request (POST/PUT/PATCH/DELETE) carrying Idempotency-Key (or the doctrine header x-nodii-idempotency-key) is claimed under a length-framed digest of (tenant, actor, "<METHOD> <path>", key):

| Situation | Result | |---|---| | Same key, same body, same actor | The first response replayed verbatim, handler NOT re-run, Idempotency-Replayed: true | | Same key, different body | 422 idempotency_key_body_mismatch (D442) | | Concurrent same key | 409 idempotency_in_flight | | Handler returned 2xx | Cached and replayed | | Any other status, or a throw | Claim RELEASED; the retry re-executes | | Response streaming / too big / too slow to drain | Delivered normally, NOT cached, claim released | | Malformed key | 400 idempotency_key_invalid | | No key, or a read verb | Passes through un-cached — the header is client-opt-in | | No resolvable tenant | 400 idempotency_tenant_required — fails closed, never a shared bucket | | No resolvable actor | 400 idempotency_actor_required — see below |

The tenant comes from c.var.tenantId and the actor from the first of actorId / userId / subjectId / sub / membershipId / principalId; supply resolveTenantId / resolveActorId for surfaces with non-JWT auth (webhook ingress, edge-stamped principals). hono is an optional peer dependency — the middleware types structurally and imports no hono.

Why the bucket is actor-scoped, and why only 2xx is cached

A replay never calls next(). That is the point of a replay, but it means everything below the mount is skipped on a hit — including route-level permission middleware. Bucketing on (tenant, route, key) alone would therefore hand one user a response another user was authorized for. Scoping to the actor makes a replay serviceable only to the principal who already passed that route's authorization when the entry was written.

The residual, stated plainly: authorization is evaluated once, when the entry is written, and a replay inside the TTL rides on that evaluation. A permission revoked mid-TTL does not invalidate the actor's own cached response. That is bounded by the TTL and leaks nothing the actor did not already hold. bucketScope: "tenant" opts out — explicitly, in code — for surfaces with no per-user principal, and then the cache is tenant-wide: any caller in the tenant presenting the same key on the same route gets the cached response without the route's own authorization running. Only set it where every caller that can reach the mount is equally privileged.

The digest is length-framed. It must stay that way: the last component is the caller's raw header, and under plain concatenation the caller owns the split point — sha256(t ‖ "POST /a" ‖ "dmin/refundsvk") equals sha256(t ‖ "POST /admin/refunds" ‖ "vk"), so for any route pair where one path prefixes the other there is a key that makes the digests collide. Never use computeIdempotencyKey (unframed, the D163 parity contract) for a request-scoped digest.

Only 2xx is cached. "Release when the handler throws" cannot work on Hono: compose catches the throw at its own depth and returns onError's response, and every nodii service ships a central error mapper — so a DB deadlock arrives here as a returned 409 and a rate-limit as a returned 429. Caching those pins the operation for the whole 24h TTL, which is the exact failure this library exists to prevent, relocated from 5xx to 4xx. Not caching a genuinely deterministic 4xx costs one re-execution that fails the same way. A service that wants a specific 4xx of its own replayed says so at the mount: cacheableStatus: (s) => (s >= 200 && s < 300) || s === 422.

The cost, stated plainly: "one re-execution that fails the same way" assumes the handler never reached its side effect. A handler that does mutate and then returns 4xx — charge the card, fail the ledger write, map to 409 — is now re-executed where it previously replayed. For that handler this trades an unrecoverable pin for a possible double effect, and the double effect is worse. Neither rule is safe there, and the old 5xx rule had the same hole. Fix it on the handler (make the mutation internally idempotent, or stop signalling a post-mutation failure with a status that means "nothing happened"); until then, opt that one status back in via cacheableStatus and accept the pin. Do not widen to "all 4xx".

Body and response limits

The D442 body fingerprint reads a CLONE of the raw Request, so a downstream c.req.json() / c.req.parseBody() behaves exactly as it would with the middleware unmounted — JSON, multipart/form-data and x-www-form-urlencoded routes are all safe to mount behind app.use("*", …). Under maxHashedRequestBytes (1 MiB) the body is buffered once and hashed canonically; over it the fingerprint switches to a streaming raw-byte hash that retains nothing, so a large upload is never made resident twice. Pass hashBody: false to skip the read entirely.

Responses are drained for caching under both a byte cap (maxCachedResponseBytes, 256 KiB) and a time cap (cacheDrainTimeoutMs, 5s), and text/event-stream / application/x-ndjson skip the drain outright. A response that exceeds either is delivered normally and simply not cached — a streaming endpoint behind the mount no longer hangs forever.

Single-active lease (@nodii/idempotency/lease)

import { SingleActiveLease } from "@nodii/idempotency/lease";

Re-exported from @nodii/telemetry/worker, where the D402 primitive lives — one implementation, two discoverable names. See that package for the full lifecycle (acquire NX, renew at ttl/2, fence token, release() in a finally) and for sizing ttlMs by crash-detection latency, not job runtime.

This is a subpath, not part of the root barrel, and that is deliberate. A root re-export makes every consumer of @nodii/idempotency depend at ESM link time on the exact export set of whatever @nodii/telemetry resolves. A peer floor that is even slightly wrong then becomes an uncatchable SyntaxError on boot in all thirteen services at once — and nothing before boot catches it (bun install warns and exits 0; tsc passes under skipLibCheck). Behind a subpath the blast radius is the one service that imported it. The surface here is pinned to what the >=0.26.0 peer floor can actually satisfy, asserted by tests/peer-floor.test.ts.

The peer floor moves from >=0.14.0 to >=0.26.0 in this release. Everything this package imports from @nodii/telemetry/worker is present in the published 0.26.0 tarball — measured with npm pack rather than assumed.

withSingleActiveLease (the renewal + fence scope function) is not exported here yet, and under this floor it must not be: the published 0.26.0 dist/worker/ does not contain it (0.27.0 is the first version that does). Adding the re-export is a separate, reviewed change that raises the floor to >=0.27.0 in the same commit.

Durable consumer-dedup (@nodii/idempotency/durable)

Comm-doctrine § 7.4 Pattern 3 + § 9.2: an inbound-EVENT consumer dedups on the domain event_id against a Postgres dedup table (a row, not a Redis key) so the dedup survives a Redis flush. This lets Pattern-3 consumers drop hand-rolled durable ledgers (PROV-SUB-IDEM).

It writes the canonical shared consumed_events table per D198 (LOCKED 2026-06-03) — the single cross-lib consumer-dedup ledger, keyed on the composite PK (event_id, topic). @nodii/approval + @nodii/replica-consumer already create/share this exact-shape table; the DDL here is byte-compatible (same columns, same PK, same ON CONFLICT (event_id, topic) DO NOTHING), so all three coexist via CREATE TABLE IF NOT EXISTS — whichever migrates first wins and they share the row. The composite key is the cross-topic-correctness key (the same event_id can recur across topics); topic is the subscriber-discriminator (no per-subscriber column, per D198).

import postgres from "postgres";
import {
  applyConsumerDedupMigration,
  claimEvent,
  wrapConsumerWorkerDurable,
  sweepConsumerDedup,
} from "@nodii/idempotency/durable";

const sql = postgres(process.env.DATABASE_URL!);
await applyConsumerDedupMigration(sql); // idempotent; boot-time

// Atomic claim — { claimed:true } first time, { claimed:false } duplicate.
const { claimed } = await claimEvent(sql, { eventId: e.event_id, topic: e.topic });
if (claimed) await process(e);

// Or wrap the whole consume lifecycle (claim → run once / skip duplicate):
await wrapConsumerWorkerDurable(sql, {
  eventId: e.event_id,
  topic: e.topic,
  handler: () => process(e),
});

// Daily retention sweep (default 30 days; run in-service per § 9.3).
await sweepConsumerDedup(sql, 30);

postgres (porsager) is an optional peer dependency — only durable-dedup users need it. Pass a sql.begin(tx => …) handle to claimEvent / tx to claim atomically with the consumer's business-state write (§ 7.4 same-tx). Ships in TS / Python (nodii_idempotency.durable, asyncpg optional extra durable) / Go (durable.go, NewDurablePgxExecutor) in parity.