New:Socket for Asana Is Now Available.Learn more
Get Started

once-kernel

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

once-kernel

Idempotency kernel for side-effecting operations. 1,000 racing callers, exactly one execution — proven, not asserted.

Source
npmnpm
Version
0.1.0
Version published
Weekly downloads
14
-57.58%
Maintainers
1
Weekly downloads
 
Created
Source

once-kernel

Idempotency for side effects that cost money. 1,000 racing callers, exactly one execution — proven on every commit, not asserted in a README.

npm install once-kernel
import { Once } from "once-kernel";
import { SqliteStore } from "once-kernel/sqlite";

const once = new Once({ store: new SqliteStore({ path: "./once.db" }) });

// Called once, twice, or by fifty racing workers — the card is charged once.
const receipt = await once.run(
  `charge:${orderId}`,        // the key
  { amount: 4900, currency: "usd" },  // the payload — part of the identity
  () => stripe.charges.create({ amount: 4900, currency: "usd" }),
);

Every caller gets the same receipt. The charge happens once.

Why this exists

The bug is not two simultaneous calls. That one is easy and everybody's in-memory Set handles it.

The bug is the worker that dies between reserving the key and finishing the effect. A Set leaves that key claimed forever and the effect never runs at all. Or the process restarts, the Set is empty, and the effect runs twice.

Retries are the other half. An agent whose request times out will re-send it. The server already did the work; the response just never arrived. Nothing failed loudly — it succeeded twice.

once handles both: a durable record, a lease that expires if you crash, and a fence token so the worker that stalled cannot overwrite the one that replaced it.

What it guarantees

  • Exactly one execution per key. Verified by 1,000 racers across real OS threads, released simultaneously by an Atomics barrier, against one shared database. The execution count is read from a separate table, not self-reported. Ten cold runs, ten times one execution.
  • Every caller gets the winner's result — not an error, not undefined.
  • A crash cannot strand a key. The lease expires and the work proceeds, with the generation counter advancing so downstream systems can fence the dead worker.
  • Same key + different payload is a conflict, not a dedupe. Guessing which body wins is how money moves twice.
  • Key order in your JSON is irrelevant. {a,b} and {b,a} are one operation — which matters when an LLM reformats its arguments between retries.

What it does not do

  • It cannot make a non-idempotent remote API idempotent. If your provider charges twice for two distinct requests, once stops the second request from being sent — it cannot un-charge one that was.
  • The default MemoryStore is single-process and dies with your program. That is fine for tests and wrong for production. Use SqliteStore, or implement the four-method Store interface against Postgres.
  • It is not a queue, a scheduler, or a retry library.

Cross-language compatibility

This is a port of once-kernel on PyPI, and it hashes payloads identically: both use RFC 8785 (JSON Canonicalization Scheme). A Python service and a Node service can key the same operation and agree about whether it already ran.

That claim is tested, not hoped for — test/vectors/python-jcs-vectors.json is generated by the Python implementation and asserted against on every commit. Hand-written expectations would only prove this file agrees with itself.

Large integers

JavaScript has one number type. By the time once sees 1234567890123456789, the parser has already rounded it to 1234567890123456800 — the precision is gone before this library is called. Pass large identifiers as strings.

API

const once = new Once({
  store,                 // default: MemoryStore
  defaultTtlSec,         // how long a completed record is remembered
  defaultLeaseSec = 30,  // how long before a crashed worker's key is reclaimed
  maxResultBytes = 65536,
});

await once.run(key, payload, fn, { ttlSec, leaseSec, waitTimeoutMs, pollMs });

Lower-level, if you need to control the boundary yourself:

const { execute, record } = await once.begin(key, payload);
if (!execute) return record.result;      // someone else already did it
try {
  const result = await doTheThing();
  await once.complete(key, record.fenceToken, result);
} catch (e) {
  await once.fail(key, record.fenceToken, String(e), /* allowRetry */ true);
  throw e;
}

complete and fail are compare-and-swap on record.fenceToken. A stalled worker that wakes up after losing its lease gets false and changes nothing.

Errors

ErrorMeaning
IdempotencyConflictSame key, different payload. A caller bug — do not retry blindly.
InProgressErrorAnother caller holds the key right now. run() waits for you.
WaitTimeoutWaited past waitTimeoutMs for an in-flight call to settle.
ResultTooLargeResult exceeds maxResultBytes. Store it elsewhere, keep a reference.
CanonicalizationErrorPayload contains something JSON cannot represent (NaN, undefined, Date, BigInt). Rejected rather than silently coerced — coercion is how two payloads collide into one hash.

Storage

SqliteStore uses Node's built-in node:sqlite. This package has zero runtime dependencies.

Every mutation is a single SQL statement with its guard in the WHERE clause, so two processes racing the same key resolve inside the database engine. Reading and then writing in JavaScript would be a time-of-check-to-time-of-use race — exactly the bug this library exists to prevent.

To use Postgres or Redis, implement Store: get, createInProgress, casComplete, casFail, reclaimIfLeaseDead, heartbeat. Run test/store-conformance.test.ts against it; that suite is the contract.

Requirements

Node 22.5+ (for node:sqlite). No build step, no native modules, no dependencies.

Licence

Apache-2.0

Keywords

idempotency

FAQs

Package last updated on 08 Aug 2026

Related posts