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 🙏

© 2025 – Pkg Stats / Ryan Hefner

steadykey

v1.0.2

Published

Deterministic idempotency key manager for JSON payloads with multi-backend storage.

Readme

steadykey

Deterministic idempotency manager for JSON payloads with pluggable persistence. Generate stable idempotency keys, prevent duplicate work, and keep canonical payloads for auditing when you need them.

Contents

  • Getting Started
  • Installation
  • Quick Tour
  • How It Works
  • API Reference
  • Storage Adapters
  • Utilities
  • Error Reference
  • Developing and Testing
  • Need Help?

Getting Started

Use IdempotencyManager to protect any workflow where repeated payloads should only be processed once. The manager stores a marker the first time it sees a payload, then lets you decide what to do when the payload returns.

import { createClient } from "redis";
import { IdempotencyManager, RedisIdempotencyStore } from "steadykey";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

const store = new RedisIdempotencyStore(redis);
const manager = new IdempotencyManager(store, {
  keyPrefix: "checkout",
  defaultTtlSeconds: 3600,
  storeCanonicalPayload: true,
});

const payload = { orderId: "order-123", total: 42.5 };

const registration = await manager.register(payload, {
  metadata: { workflow: "checkout" },
});

if (registration.stored) {
  // First encounter: perform the expensive work and persist your result.
  // Later you can call manager.clear(id) or manager.updateTtl(id, ttl) when done.
} else {
  // Duplicate payload: skip the work and reuse the prior result.
}

For quick checks, call steadyKey(payload) to get a deterministic hash without creating a manager.

Installation

Install the core package plus the adapter dependencies your project uses.

npm install steadykey

# Optional adapter helpers
npm install redis             # RedisIdempotencyStore
npm install memcached         # MemcachedIdempotencyStore
npm install pg                # PostgresIdempotencyStore
npm install mysql2            # MySqlIdempotencyStore
npm install mongodb           # MongoIdempotencyStore
npm install better-sqlite3    # SqliteIdempotencyStore
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb  # DynamoDbIdempotencyStore

Quick Tour

  • IdempotencyManager orchestrates key generation, storage, TTL management, and collision detection.
  • Storage adapters implement the lightweight IdempotencyStore interface so you can bring your own persistence layer.
  • Utility helpers (steadyKey, canonicalize, hashCanonicalValue) let you generate and inspect deterministic payload hashes outside of a full manager.
  • Typed results explain whether the current call was stored (stored: true) or matches an existing record (stored: false).

How It Works

  1. Payloads are canonicalized before hashing. Object keys are sorted, undefined values are dropped, Maps/Sets/BigInts/Buffers are normalized, and Dates become ISO strings. Identical logical payloads always hash to the same value.
  2. The chosen hash algorithm (sha256 by default) creates an idempotency identifier.
  3. IdempotencyManager prefixes the identifier (default idempotency:) to build the storage key.
  4. The storage adapter stores the record if the key is not already present. When the key exists, the stored payload hash is compared to guard against silent collisions.
  5. TTLs come either from the manager constructor (defaultTtlSeconds), from each registration call, or can be removed entirely by passing null or 0.

API Reference

steadyKey(payload, options?)

  • Returns a deterministic string hash for any JSON-like payload.
  • options.hashAlgorithm accepts "sha256" (default) or "sha512".
import { steadyKey } from "steadykey";

const key = steadyKey({ customerId: 123, items: ["A", "B"] });
// same key every call, regardless of object key order

class IdempotencyManager

const manager = new IdempotencyManager(store, options?);
  • store must satisfy the IdempotencyStore interface (see adapters below).
  • options.keyPrefix (string) defaults to "idempotency". Trailing colons are trimmed automatically.
  • options.defaultTtlSeconds (positive integer | null | undefined) sets the fallback TTL. null or undefined means no expiration.
  • options.hashAlgorithm overrides the hashing algorithm used for the manager ("sha256" or "sha512").
  • options.storeCanonicalPayload stores the canonical JSON alongside the record to help with auditing or debugging.

generateId(payload)

Returns the deterministic hash for a payload using the manager's algorithm. Useful if you want to build keys or pre-compute lookups.

buildKey(id)

Combines keyPrefix and an id into the stored key. The legacy alias buildRedisKey is still available but deprecated.

register(payload, options?)

Stores a record the first time the payload is seen.

const result = await manager.register(payload, {
  ttlSeconds: 900,
  metadata: { workflow: "checkout" },
  storeCanonicalPayload: false,
});

if (result.stored) {
  // process payload
}
  • options.ttlSeconds overrides the manager default for this call.
  • options.metadata accepts any JSON-serializable value (objects, strings, numbers, etc.) and is stored with the record.
  • options.storeCanonicalPayload toggles payload storage per call.
  • Result shape: { id, key, stored, record } where record reflects the stored data (including metadata and canonical payload when present).

lookupByPayload(payload) / lookupById(id)

Fetch existing records without registering anything. Returns null when no record is found.

const previous = await manager.lookupByPayload(payload);
if (previous) {
  console.log(previous.record.metadata);
}
  • Lookup results include { id, key, record, ttlSeconds } where ttlSeconds comes from the backing store when available.

updateTtl(id, ttlSeconds)

Refreshes, sets, or removes the TTL for an existing record. Pass null or 0 to make the record persistent. Throws when the key does not exist.

clear(id)

Deletes the stored record. Returns true when a record was removed.

Records and Data Shapes

  • IdempotencyRecord: { id, payloadHash, createdAt, metadata?, canonicalPayload?, ttlSeconds? }
  • IdempotencyRegistrationResult: { id, key, stored, record }
  • IdempotencyLookupResult: { id, key, record, ttlSeconds? }
  • HashAlgorithm: union of "sha256" | "sha512"

These types are exported from steadykey so you can annotate your code when TypeScript type safety matters.

Creating Custom Stores

Implement the IdempotencyStore interface if you need a bespoke persistence layer.

interface IdempotencyStore {
  setIfAbsent(key: string, value: string, ttlSeconds: number | null): Promise<boolean>;
  get(key: string): Promise<{ value: string; ttlSeconds?: number | null } | null>;
  update(key: string, value: string, ttlSeconds: number | null): Promise<void>;
  delete(key: string): Promise<boolean>;
}
  • setIfAbsent must behave atomically: only return true when the key did not exist.
  • get should ignore expired entries and return their TTL when known.
  • update must throw when the key is missing to avoid silently masking data issues.
  • delete should return whether the key was removed.

Storage Adapters

InMemoryIdempotencyStore

  • Lightweight Map-based implementation ideal for tests.
  • Constructor accepts { now?: () => number } for deterministic time sources.
  • Exposes advanceTime(milliseconds) to fast-forward expirations in tests.
import { InMemoryIdempotencyStore } from "steadykey";

const store = new InMemoryIdempotencyStore();
const manager = new IdempotencyManager(store);

store.advanceTime(5_000); // simulate clock jumps in unit tests

RedisIdempotencyStore

  • Wraps a redis client with set, get, ttl, persist, and del methods.
  • Pass TTLs via EX so expirations are handled server-side.
  • update removes TTLs when ttlSeconds is null.
const redisStore = new RedisIdempotencyStore(redisClient);

MemcachedIdempotencyStore

  • Works with clients compatible with the memcached npm package.
  • TTL reporting is not available, so lookups return ttlSeconds: undefined.
  • Uses add for atomic set-if-absent operations.
const memcachedStore = new MemcachedIdempotencyStore(memcachedClient);

PostgresIdempotencyStore

  • Requires any client exposing a query(sql, params) method (e.g., pg.Pool).
  • Options: { tableName?: string, ensureTable?: boolean }.
  • Defaults to creating steadykey_entries with an expires_at index. Disable auto-DDL with ensureTable: false.
const pgStore = new PostgresIdempotencyStore(pool, {
  tableName: "public.steadykey_entries",
});

MySqlIdempotencyStore

  • Works with mysql2/promise connections.
  • Options: { tableName?: string, ensureTable?: boolean, keyLength?: number }.
  • Auto-DDL creates an indexed table with configurable primary key length.
const mysqlStore = new MySqlIdempotencyStore(connection, {
  tableName: "steadykey_entries",
  keyLength: 128,
});

MongoIdempotencyStore

  • Accepts a MongoDB collection implementing insertOne, findOne, updateOne, deleteOne, and createIndex.
  • Options: { ensureIndexes?: boolean }. Defaults to building a TTL index on expiresAt.
const mongoStore = new MongoIdempotencyStore(collection, {
  ensureIndexes: true,
});

DynamoDbIdempotencyStore

  • Works with AWS SDK v3 DynamoDBDocumentClient or compatible clients exposing put, get, update, and delete.
  • Options: { tableName: string, partitionKey?: string, valueAttribute?: string, ttlAttribute?: string, consistentRead?: boolean }.
  • Uses conditional writes for atomic inserts and stores TTL as epoch seconds when provided.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DynamoDbIdempotencyStore } from "steadykey";

const client = new DynamoDBClient({});
const documentClient = DynamoDBDocumentClient.from(client);

const dynamoStore = new DynamoDbIdempotencyStore(documentClient, {
  tableName: "steadykey_entries",
});

SqliteIdempotencyStore

  • Compatible with synchronous libraries such as better-sqlite3 or async wrappers that match the minimal interface.
  • Options: { tableName?: string, ensureTable?: boolean }.
  • Automatically creates a table keyed by key with an index on expires_at (epoch seconds).
const sqliteStore = new SqliteIdempotencyStore(sqliteDb, {
  tableName: "steadykey_entries",
});

Utilities

  • canonicalize(value) returns the deterministic JSON string used for hashing. Useful for debugging when combined with storeCanonicalPayload.
  • hashCanonicalValue(canonicalValue, algorithm) hashes previously canonicalized JSON. This is exported for advanced integrations or to align custom tooling with Steadykey.
import { canonicalize, hashCanonicalValue } from "steadykey";

const canonical = canonicalize(payload);
const id = hashCanonicalValue(canonical, "sha512");

Error Reference

  • IdempotencyError: thrown for invalid input or misconfigured stores.
  • IdempotencyCollisionError: thrown when two different payloads attempt to reuse the same key.
  • IdempotencySerializationError: wraps canonicalization or JSON serialization issues. Inspect the message for the underlying cause.

Always surface collisions and serialization errors in logs or metrics—they indicate data drift or payloads the hashing strategy cannot support yet.

Developing and Testing

  • Run unit tests with npm test (Vitest).
  • Build distributable bundles with npm run build (outputs ESM, CJS, and type declarations under dist/).
  • Build once before running the Node examples under examples/ (they import from dist/index.js).
  • When adding new storage backends, implement the IdempotencyStore contract and add adapter-specific tests under tests/.

Need Help?

  • Open an issue or discussion in the repository with payload samples and adapter details.
  • Pull requests are welcome—please include tests and update this README when the API surface changes.