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

simloop

v0.2.0

Published

A general-purpose discrete event simulation framework for Node.js

Readme

Simloop

npm version license build npm bundle size types

A general-purpose discrete event simulation (DES) framework for Node.js, written in TypeScript.

Simloop provides a minimal, type-safe API for building simulations of real-world systems. You define events, entities, and handlers — the framework runs the event loop.

Features

  • Type-safe — generic TEventMap gives full autocomplete and type checking on event scheduling and handling
  • Zero runtime dependencies — only Node.js built-ins
  • Deterministic — seeded PRNG ensures reproducible results
  • Simple API — define handlers with sim.on(), schedule events with ctx.schedule()
  • Probability distributions — uniform, gaussian, exponential, poisson, bernoulli, zipf, triangular, weibull, lognormal, erlang, geometric
  • Lifecycle management — run, pause, resume, stop, reset
  • Built-in statistics — online mean, variance, min, max, count
  • Pluggable logging — bring your own logger or use the default console logger
  • Dual module format — ESM and CJS

Installation

npm install simloop

Quick Start

import { SimulationEngine, exponential } from 'simloop';

// 1. Define your event types
type Events = {
  'customer:arrive': { customerId: string };
  'customer:serve': { customerId: string };
};

// 2. Create the engine
const sim = new SimulationEngine<Events>({ seed: 42, maxTime: 100 });

// 3. Register handlers
sim.on('customer:arrive', (event, ctx) => {
  ctx.stats.increment('arrivals');

  // serve immediately
  ctx.schedule('customer:serve', ctx.clock + 2, {
    customerId: event.payload.customerId,
  });

  // schedule next arrival (exponential inter-arrival, mean = 5)
  const nextArrival = exponential(() => ctx.random(), 0.2);
  ctx.schedule('customer:arrive', ctx.clock + nextArrival(), {
    customerId: `C${ctx.stats.get('arrivals').count + 1}`,
  });
});

sim.on('customer:serve', (event, ctx) => {
  ctx.stats.increment('served');
});

// 4. Initialize and run
sim.init((ctx) => {
  ctx.schedule('customer:arrive', 0, { customerId: 'C1' });
});

const result = sim.run();
console.log(result.stats);

Core Concepts

Events

Events are timestamped actions with a type tag and a payload. The TEventMap generic maps each event type to its payload shape:

type MyEvents = {
  'order:placed': { orderId: string; items: number };
  'order:completed': { orderId: string };
};

Entities

Entities are stateful objects that participate in the simulation. They have a unique id and a generic state:

ctx.addEntity({ id: 'server-1', state: { busy: false, processed: 0 } });

const server = ctx.getEntity<{ busy: boolean; processed: number }>('server-1');
server!.state.busy = true;

Simulation Context

Every handler receives a SimContext with:

| Method | Description | |---|---| | ctx.clock | Current simulation time | | ctx.schedule(type, time, payload) | Schedule a new event | | ctx.cancelEvent(event) | Cancel a scheduled event | | ctx.getEntity(id) | Get an entity by ID | | ctx.addEntity(entity) | Add an entity | | ctx.removeEntity(id) | Remove an entity | | ctx.store | Global simulation store (typed as TStore) | | ctx.stats | Statistics collector (numeric metrics) | | ctx.random() | Seeded random number (0-1) | | ctx.log(level, message) | Log a message |

Global Store

The store is a typed, persistent object for accumulating custom data across handlers and hooks. Initialize it via options.store and access it as ctx.store. It's returned in SimulationResult and restored to its initial value on reset().

type Events = { tick: { value: number } };
type Store  = { count: number; total: number };

const sim = new SimulationEngine<Events, Store>({
  store: { count: 0, total: 0 },
});

sim.on('tick', (event, ctx) => {
  ctx.store.count++;
  ctx.store.total += event.payload.value;
});

const result = sim.run();
console.log(result.store); // { count: ..., total: ... }

Event Cancellation

schedule() returns the event object. Pass it to cancelEvent() to prevent it from being processed:

const timeout = ctx.schedule('timeout', ctx.clock + 10, {});
// later...
ctx.cancelEvent(timeout);

Lifecycle Hooks

sim.beforeEach((event, ctx) => { /* before each event */ });
sim.afterEach((event, ctx) => { /* after each event */ });
sim.onEnd((ctx) => { /* when simulation finishes */ });

Configuration

const sim = new SimulationEngine<Events, Store>({
  seed: 42,            // PRNG seed (default: Date.now())
  maxTime: 1000,       // stop at this simulation time (default: Infinity)
  maxEvents: 5000,     // stop after N events (default: Infinity)
  logLevel: 'info',    // 'debug' | 'info' | 'warn' | 'error' | 'silent'
  name: 'MySim',       // log prefix (default: 'Simulation')
  realTimeDelay: 100,  // ms delay between events in runAsync (default: 0)
  store: { ... },      // initial global store value (default: {})
});

Simulation Result

run() returns a SimulationResult:

const result = sim.run();

result.totalEventsProcessed  // number of events handled
result.totalEventsCancelled  // number of cancelled events skipped
result.finalClock            // final simulation time
result.wallClockMs           // real-world execution time in ms
result.stats                 // Record<string, StatsSummary>
result.status                // 'finished' | 'stopped' | 'maxTimeReached' | 'maxEventsReached'
result.store                 // TStore — final state of the global store

Async Execution

For long simulations that shouldn't block the Node.js event loop:

const result = await sim.runAsync();

Resource

Resource implements the seize/delay/release pattern for capacity-constrained shared resources — the building block of M/M/c queueing models (servers, machines, staff, connections).

import { SimulationEngine, Resource, exponential } from 'simloop';

type Events = {
  'job:arrive': { jobId: number };
  'job:done':   Record<string, never>;
};

const sim = new SimulationEngine<Events>({ seed: 42 });
const server = new Resource<Events>('server'); // capacity defaults to 1

sim.on('job:arrive', (event, ctx) => {
  const arrivalTime = ctx.clock;

  // SEIZE — callback fires when a slot is free (immediately or after queuing)
  server.request(ctx, (ctx) => {
    ctx.stats.record('waitTime', ctx.clock - arrivalTime);
    ctx.schedule('job:done', ctx.clock + exponential(() => ctx.random(), 1)(), {});
  });

  ctx.schedule('job:arrive', ctx.clock + exponential(() => ctx.random(), 0.8)(), {
    jobId: event.payload.jobId + 1,
  });
});

sim.on('job:done', (_e, ctx) => {
  server.release(ctx); // RELEASE — automatically grants next queued request
});

Auto-collected statistics: resource.{name}.waitTime, queueLength, utilization, requests, grants.

For the full API — priority queuing, cancellation, edge cases, and M/M/c examples — see docs/resource-spec.md.

Examples

See the examples/ directory:

  • store-counter — minimal example showing ctx.store usage
  • coffee-shop — multi-barista coffee shop with customer patience, drink types, and queue management
  • network-packets — network router simulation using all six probability distributions
npm run example:store-counter
npm run example:coffee-shop
npm run example:network-packets

Probability Distributions

Simloop includes common probability distributions as composable factory functions. Each takes a () => number source (like ctx.random) and returns a sampler:

import { SimulationEngine, exponential, gaussian, bernoulli } from 'simloop';

const sim = new SimulationEngine<Events>({ seed: 42 });

sim.on('customer:arrive', (event, ctx) => {
  const nextArrival = exponential(() => ctx.random(), 0.5);
  const serviceTime = gaussian(() => ctx.random(), 10, 2);

  ctx.schedule('customer:arrive', ctx.clock + nextArrival(), { ... });
});

| Distribution | Factory | Description | |---|---|---| | Uniform | uniform(rng, a, b) | Continuous on [a, b) | | Gaussian | gaussian(rng, mean?, stddev?) | Normal via Box-Muller (default: standard normal) | | Exponential | exponential(rng, rate) | Rate λ, mean = 1/λ | | Poisson | poisson(rng, lambda) | Non-negative integers, mean = λ | | Bernoulli | bernoulli(rng, p) | Returns 1 with probability p, 0 otherwise | | Zipf | zipf(rng, n, s) | Ranks [1, n], probability ∝ 1/k^s | | Triangular | triangular(rng, min, mode, max) | Three-point estimate; useful when only min/mode/max are known | | Weibull | weibull(rng, scale, shape) | Reliability and failure analysis; shape controls failure rate regime | | Lognormal | lognormal(rng, mu?, sigma?) | Right-skewed; models service times, repair durations, response times | | Erlang | erlang(rng, k, rate) | Sum of k exponentials; models k-stage sequential processes | | Geometric | geometric(rng, p) | Trials until first success; minimum value is 1 |

API Reference

Exported Classes

  • SimulationEngine<TEventMap, TStore> — main simulation engine
  • Resource<TEventMap, TStore> — seize/delay/release primitive for shared resources
  • SimulationError — error thrown for invalid operations
  • ConsoleLogger — default logger implementation
  • DefaultStatsCollector — default statistics collector
  • SeededRandom — Mulberry32 PRNG

Exported Distribution Functions

  • uniform(rng, a, b) — continuous uniform
  • gaussian(rng, mean?, stddev?) — normal (Box-Muller)
  • exponential(rng, rate) — exponential
  • poisson(rng, lambda) — Poisson (Knuth)
  • bernoulli(rng, p) — Bernoulli
  • zipf(rng, n, s) — Zipf
  • triangular(rng, min, mode, max) — triangular
  • weibull(rng, scale, shape) — Weibull
  • lognormal(rng, mu?, sigma?) — lognormal
  • erlang(rng, k, rate) — Erlang
  • geometric(rng, p) — geometric

Exported Types

  • SimEvent<TType, TPayload> — simulation event
  • SimEntity<TState> — simulation entity
  • SimContext<TEventMap, TStore> — handler context
  • EventHandler<TEventMap, TType, TStore> — handler function signature
  • SimulationResult<TStore> — run result
  • SimulationEngineOptions<TStore> — engine configuration
  • ResourceOptions / RequestOptions / RequestHandle / ResourceSnapshot — Resource types
  • StatsCollector / StatsSummary — statistics interfaces
  • SimLogger / LogLevel — logging interfaces
  • SimulationStatus / SimulationEndStatus — lifecycle types

License

MIT