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

@simnode/simnode

v0.1.1-alpha.0

Published

**Find 1-in-a-million race conditions in milliseconds, not months.**

Readme

SimNode — Deterministic Simulation Testing for Node.js

Find 1-in-a-million race conditions in milliseconds, not months.

SimNode runs your application code inside a fully controlled simulation: virtual time, seeded randomness, and deterministic I/O scheduling. Every concurrency bug that would normally require weeks of load testing to surface can be reproduced on demand, debugged with a single seed, and guarded against regression forever.


The Problem with Conventional Testing

Imagine a payment handler:

async function charge(userId: string, amount: number) {
  const balance = await db.query('SELECT balance FROM accounts WHERE id = $1', [userId]);
  if (balance.rows[0].balance < amount) throw new Error('Insufficient funds');
  await stripe.charge(userId, amount);               // ~200ms network call
  await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, userId]);
}

A double-payment race condition is buried here. Two concurrent requests both read the same balance, both pass the guard, and both charge the card — but only one debits the account. This bug requires two requests to arrive within a ~200ms window. In a Jest or Vitest test suite, your async calls resolve sequentially; the window never opens and the test always passes.

SimNode compresses virtual time and shuffles I/O resolution order. Across 1,000 seeds it explores every possible interleaving of those two awaits. Seed 847 opens the exact window. You get a failing test, a full timeline, and a replay command — before this ships.


Example

scenarios/charge.scenario.ts — the file your team ships alongside the code:

import type { SimEnv } from '@simnode/simnode';

export default async function chargeScenario(env: SimEnv) {
  // Mock Stripe: 200ms virtual latency, deterministic response
  env.http.mock('POST https://api.stripe.com/v1/charges', {
    status: 200,
    body: JSON.stringify({ id: 'ch_sim', status: 'succeeded' }),
    latency: 200,
  });

  // Seed Postgres with a user who has $100
  env.pg.seedData('accounts', [{ id: 'user_1', balance: 100 }]);
  await env.pg.ready();

  // Fire two concurrent charge requests at the same virtual instant
  const req = () =>
    fetch('http://localhost:3000/charge', {
      method: 'POST',
      body: JSON.stringify({ userId: 'user_1', amount: 100 }),
    });

  const [r1, r2] = await Promise.all([req(), req()]);

  // Advance virtual clock past the Stripe latency — both callbacks resolve
  await env.clock.advance(250);

  const result = await env.pg.query<{ balance: number }>(
    'SELECT balance FROM accounts WHERE id = $1', ['user_1']
  );

  env.timeline.record({
    timestamp: env.clock.now(),
    type: 'ASSERT',
    detail: `final balance: ${result.rows[0].balance}`,
  });

  // The balance must be 0 — any other value is a double-charge
  if (result.rows[0].balance !== 0) {
    throw new Error(`Double charge detected! Balance is ${result.rows[0].balance}, expected 0`);
  }
}

simnode.config.js — wire it to the harness:

import { Simulation } from '@simnode/simnode';
import { resolve } from 'node:path';

const sim = new Simulation({ timeout: 15_000 });

sim.scenario('double charge guard', resolve('./scenarios/charge.scenario.ts'));

export default sim;

How It Works — The Three Pillars

1. Virtual Clock

Date.now(), performance.now(), setTimeout, and setInterval are replaced with a fully controllable VirtualClock. Time only moves when you call env.clock.advance(ms). A scenario that would take 200ms in production takes 0 wall-clock milliseconds in simulation.

2. Seeded PRNG

Math.random() and crypto.randomBytes() are replaced with a deterministic Mulberry32-based generator seeded per-run. Given the same seed, every random value produced during the scenario is identical — every time, on every machine.

3. I/O Scheduler

Concurrent await calls that resolve at the same virtual timestamp are queued and shuffled by the seed before being delivered. Seed 0 might resolve DB-then-Stripe. Seed 847 resolves Stripe-then-DB. Running 1,000 seeds explores 1,000 distinct interleavings of every concurrent I/O operation in your code.

These three pillars together mean: if a race condition is possible, a seed will find it.


CLI Usage

SimNode has two operating modes and a replay command:

| | run | hunt | |---|---|---| | What you specify | Seed count | Time budget | | When it stops | After N seeds (or first failure by default) | On first failure or timeout | | What it outputs | Pass/fail summary with counts | Live per-seed status, full failure report | | Memory | Only failures retained | Never accumulates passing results | | When to use | CI / regression suites | Local debugging, "find me a bug" sessions |


simnode run — fixed seed count, CI mode

npx simnode run --seeds=1000

Stops at the first failure by default (stopOnFirstFailure: true). To collect all failures across all seeds:

npx simnode run --seeds=1000 --stop-on-first-failure=false

Output:

✗ [seed=847] double charge guard: Double charge detected! Balance is 100, expected 0
  Timeline:
    [0ms]    START: Scenario: double charge guard, seed: 847
    [0ms]    DB:    SELECT balance → 100  (request A)
    [0ms]    DB:    SELECT balance → 100  (request B)
    [200ms]  HTTP:  POST /v1/charges → succeeded  (request A)
    [200ms]  HTTP:  POST /v1/charges → succeeded  (request B)
    [200ms]  DB:    UPDATE balance = 0  (request A)
    [200ms]  DB:    UPDATE balance = 0  (request B)
    [200ms]  ASSERT: final balance: 100
    [200ms]  FAIL:  Double charge detected! Balance is 100, expected 0

0/1000 passed, 1 failed

simnode hunt — time-budget mode, local debugging

Hunt mode runs as many seeds as it can fit within a time budget and stops the moment it finds a failure. There is no seed count — just run until you find something.

npx simnode hunt ./scenarios/charge.scenario.ts
npx simnode hunt ./scenarios/charge.scenario.ts --timeout=10m

Duration format: 30s | 5m | 1h. Default: 5m.

Live output:

Hunting: charge.scenario.ts  (timeout: 5m)

[OK  ] Seed 482910341
[OK  ] Seed 482910342
[OK  ] Seed 482910343
[FAIL] Seed 482910344

────────────────────────────────────────────────────────────
FAILURE FOUND after 4 seeds in 2s
  Scenario : charge.scenario
  Seed     : 482910344
  Error    : Double charge detected! Balance is 100, expected 0

Timeline:
  [0ms]  START: ...
  ...

Replay command:
  simnode replay --seed=482910344 --scenario="charge.scenario" --config=simnode.config.js

If no failure is found within the budget:

No failure found after 1247 seeds in 5m 0s (timeout after 5m 0s).
Your scenario may be correct, or the bug requires a specific condition not yet explored.

Press Ctrl+C to stop early — SimNode finishes the current seed, discards its result (it may have been interrupted mid-flight), and exits cleanly with code 0:

No failure found after 1247 seeds in 2m 14s (interrupted by Ctrl+C).

simnode replay — reproduce a specific failure

npx simnode replay --seed=847 --scenario="double charge guard"

[!TIP] The same seed always produces the same failure. You can share --seed=847 with a colleague, add it to a CI regression suite, or step through it in a debugger. The entire execution is deterministic.


sim.run() API — programmatic use

// Default: stop on first failure, only store failing results
const result = await sim.run({ seeds: 1000 });
// result.passed    → boolean
// result.passes    → number of seeds that passed
// result.failures  → ScenarioResult[] (only failures; passing results are not retained)

// Opt out of early stop to collect all failures
const result = await sim.run({ seeds: 1000, stopOnFirstFailure: false });

// Replay always returns the full result regardless of pass/fail
const replay = await sim.replay({ seed: 847, scenario: 'double charge guard' });
// replay.passed    → boolean
// replay.result    → ScenarioResult (always present, including timeline)

Custom config path

npx simnode run --config=./tests/sim/simnode.config.js --seeds=500

Installation

Batteries-included (recommended)

npm install --save-dev @simnode/simnode

@simnode/simnode includes every mock: Postgres (PGlite), MongoDB (MongoMemoryServer), Redis (ioredis-mock), HTTP, TCP, virtual clock, PRNG, filesystem. Install this and you are done.

À la carte

If you only need a subset of the mocks — say, virtual time and HTTP interception with no database overhead — install the lightweight engine and only the layers you need:

npm install --save-dev @simnode/core @simnode/clock @simnode/http-proxy

@simnode/core ships the Simulation class, CLI runner, and worker engine. It has no dependency on PGlite, MongoDB, or Redis. Mocks that are not installed simply appear as null on env.pg, env.redis, and env.mongo.

Available sub-packages:

| Package | What it provides | |---|---| | @simnode/core | Simulation class, CLI, worker engine | | @simnode/clock | VirtualClock | | @simnode/random | SeededRandom | | @simnode/scheduler | Scheduler | | @simnode/http-proxy | HttpInterceptor | | @simnode/tcp | TcpInterceptor | | @simnode/filesystem | VirtualFS | | @simnode/pg-mock | PgMock (PGlite) | | @simnode/redis-mock | RedisMock (ioredis-mock) | | @simnode/mongo | MongoMock (MongoMemoryServer) |


Support Matrix

| Protocol / Driver | SimNode Support | Notes | |---|---|---| | PostgreSQL | ✅ Full | PGlite in-process — wire-protocol compatible | | MongoDB | ✅ Full | Proxied to MongoMemoryServer per-run | | Redis | ✅ Full | In-process RESP protocol handler | | HTTP / Fetch | ✅ Full | http.request, https.request, globalThis.fetch | | Prisma | ✅ Compatible | Loopback TCP servers on 5432 / 27017 / 6379 | | ioredis / mongoose / pg | ✅ Compatible | Client-side module patch — zero config | | MySQL | ❌ Not supported | Port 3306 throws SimNodeUnsupportedProtocolError in v1.0 |

[!NOTE] Prisma compatibility: SimNode binds real loopback TCP servers on 127.0.0.1:5432, :6379, and :27017, so Prisma's out-of-process Rust query engine connects to the same mocks as your in-process drivers. If a real database is running on those ports, SimNode records a WARNING in the timeline and falls back to the client-side interceptor.


Honest Limitations

SimNode is precise about what it controls. Senior engineers deserve a straight answer:

| Limitation | Reason | |---|---| | Native C++ addons | Native code runs outside the V8 sandbox. require('better-sqlite3') or bcrypt bypass all module patches. Use pure-JS alternatives in scenarios, or wrap them in an HTTP service that SimNode can mock. | | Engine-level microtask interleaving | V8's microtask queue is not observable from userland. SimNode controls macro-task and I/O scheduling; it cannot reorder Promise.resolve() chains that don't yield to the event loop. | | worker_threads spawned by your app | Child workers inherit real globals, not SimNode's patched ones. Scenarios should avoid code paths that spawn workers; use the simulation environment's own concurrency tools instead. | | True wall-clock timers | Any library that calls the real setTimeout before SimNode installs its patch (e.g. at module evaluation time) will use real time. Import order matters. |


Scenario API Reference

import type { SimEnv } from '@simnode/simnode';

export default async function myScenario(env: SimEnv) {
  env.clock         // VirtualClock — advance(), now(), setTimeout(), setInterval()
  env.random        // SeededRandom — next() → [0,1), nextInt(n)
  env.scheduler     // Scheduler — enqueueCompletion(), runTick()
  env.http          // HttpInterceptor — mock(), calls, unmatched handling
  env.tcp           // TcpInterceptor — mock(), addLocalServer()
  env.pg            // PgMock — seedData(), query(), ready(), createHandler()
  env.redis         // RedisMock — seedData(), createHandler()
  env.mongo         // MongoMock — find(), drop(), createHandler()
  env.fs            // VirtualFS — readFileSync(), writeFileSync(), existsSync()
  env.faults        // FaultInjector — diskFull(), clockSkew(), networkPartition()
  env.timeline      // Timeline — record({ timestamp, type, detail })
  env.seed          // number — the current run's seed value
}

Architecture

Package split

npm install @simnode/simnode      ← batteries-included (re-exports @simnode/core + all mocks)
npm install @simnode/core         ← lightweight engine only (no PGlite / MongoDB / Redis)

@simnode/core declares the heavy mock packages as optional peer dependencies. If they are not installed env.pg, env.redis, and env.mongo are null. The @simnode/simnode wrapper lists them as required dependencies, guaranteeing they are always present.

Runtime flow

Simulation.run({ seeds: N })          ← lives in @simnode/core
│
├─ _startMongo()                      → MongoMemoryServer (skipped if not installed)
│
└─ for each seed × scenario
   └─ Worker thread (isolated globals)
      ├─ createEnv(seed)              → VirtualClock, PRNG, Scheduler, lightweight mocks
      │   ├─ try import('@simnode/pg-mock')    → PgMock  | null
      │   ├─ try import('@simnode/redis-mock') → RedisMock | null
      │   └─ try import('@simnode/mongo')      → MongoMock | null
      ├─ install patches              → Date.now, Math.random, net.createConnection, fetch
      ├─ import(scenario)             → dynamic ES module load (file-based)
      ├─ await scenarioFn(env)
      ├─ timeline.toString()          → posted to parent
      └─ finally: uninstall patches, drop mongo db, worker.terminate()

Each worker is fully isolated. Patches applied inside one worker never leak to the main thread or sibling workers. After run() returns there are zero zombie workers and zero mongod instances.


Contributing

git clone https://github.com/your-org/simnode
npm install
npm run build
npm test           # vitest — 176 tests

All packages live under packages/. The monorepo uses npm workspaces + tsup for building. PRs must pass npm test with zero failures.

Releasing

This repo uses Changesets for versioning and publishing. All packages are versioned together (fixed group).

# 1. Describe your change (prompts for bump type + summary)
npx changeset

# 2. Apply version bumps — updates all package.json versions and cross-package pins
npm run version

# 3. Build and publish to npm
npm run release

License

MIT