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

@hamicek/noex

v0.1.1

Published

Elixir-style GenServer and Supervisor patterns for TypeScript

Readme

@hamicek/noex

Elixir-style GenServer and Supervisor patterns for TypeScript.

noex provides a robust abstraction for building stateful, fault-tolerant services in Node.js. Inspired by Elixir/OTP, it brings the GenServer and Supervisor patterns to TypeScript with full type safety.

Features

  • GenServer: Stateful services with serialized message processing
  • Supervisor: Automatic restart strategies for fault tolerance
  • Registry: Named process lookup for loose coupling
  • Built-in Services: EventBus, Cache, RateLimiter, TimerService — all built on GenServer
  • Observer: Real-time introspection into process state
  • Dashboard: TUI-based monitoring interface
  • Distribution (experimental): Multi-node clustering with transparent remote calls
  • Type-safe: Full TypeScript support with strict typing
  • Zero dependencies: Core library is lightweight and focused

Installation

npm install @hamicek/noex

Requires Node.js 20.0.0 or later.

Quick Start

import { GenServer, Supervisor, Registry } from 'noex';

// Define a counter service
const counterBehavior = {
  init: () => 0,
  handleCall: (msg: 'get', state: number) => [state, state] as const,
  handleCast: (msg: 'inc' | 'dec', state: number) =>
    msg === 'inc' ? state + 1 : state - 1,
};

// Start under supervision
const supervisor = await Supervisor.start({
  strategy: 'one_for_one',
  children: [
    {
      id: 'counter',
      start: async () => {
        const ref = await GenServer.start(counterBehavior);
        Registry.register('counter', ref);
        return ref;
      },
    },
  ],
});

// Use the service
const counter = Registry.lookup<number, 'get', 'inc' | 'dec', number>('counter');
GenServer.cast(counter, 'inc');
GenServer.cast(counter, 'inc');
const value = await GenServer.call(counter, 'get'); // 2

// Graceful shutdown
await Supervisor.stop(supervisor);

Core Concepts

GenServer

A GenServer is a stateful process that handles messages sequentially. It provides:

  • Serialized message processing: Messages are processed one at a time
  • Synchronous calls: Request/response pattern with timeouts
  • Asynchronous casts: Fire-and-forget messages
  • Lifecycle hooks: Initialization and termination callbacks
import { GenServer, type GenServerBehavior } from 'noex';

interface CacheState {
  data: Map<string, unknown>;
}

type CallMsg = { type: 'get'; key: string } | { type: 'keys' };
type CastMsg = { type: 'set'; key: string; value: unknown } | { type: 'delete'; key: string };
type CallReply = unknown | string[];

const cacheBehavior: GenServerBehavior<CacheState, CallMsg, CastMsg, CallReply> = {
  init: () => ({ data: new Map() }),

  handleCall: (msg, state) => {
    if (msg.type === 'get') {
      return [state.data.get(msg.key), state];
    }
    return [Array.from(state.data.keys()), state];
  },

  handleCast: (msg, state) => {
    if (msg.type === 'set') {
      state.data.set(msg.key, msg.value);
    } else {
      state.data.delete(msg.key);
    }
    return state;
  },

  terminate: (reason, state) => {
    console.log(`Cache shutting down: ${reason}`);
  },
};

const cache = await GenServer.start(cacheBehavior);

GenServer.cast(cache, { type: 'set', key: 'user:1', value: { name: 'Alice' } });
const user = await GenServer.call(cache, { type: 'get', key: 'user:1' });

await GenServer.stop(cache);

Supervisor

A Supervisor manages child processes and restarts them on failure. It supports three restart strategies:

  • one_for_one: Only restart the failed child
  • one_for_all: Restart all children when one fails
  • rest_for_one: Restart the failed child and all children started after it
import { Supervisor, GenServer } from 'noex';

const supervisor = await Supervisor.start({
  strategy: 'one_for_one',
  restartIntensity: { maxRestarts: 3, withinMs: 5000 },
  children: [
    {
      id: 'worker-1',
      restart: 'permanent', // Always restart
      start: () => GenServer.start(workerBehavior),
    },
    {
      id: 'worker-2',
      restart: 'transient', // Restart only on abnormal exit
      start: () => GenServer.start(workerBehavior),
    },
    {
      id: 'worker-3',
      restart: 'temporary', // Never restart
      start: () => GenServer.start(workerBehavior),
    },
  ],
});

// Dynamic child management
await Supervisor.startChild(supervisor, {
  id: 'worker-4',
  start: () => GenServer.start(workerBehavior),
});

await Supervisor.terminateChild(supervisor, 'worker-4');

// Introspection
const children = Supervisor.getChildren(supervisor);
console.log(`Managing ${children.length} children`);

await Supervisor.stop(supervisor);

Registry

The Registry provides named process lookup, enabling loose coupling between components:

import { Registry, GenServer } from 'noex';

const ref = await GenServer.start(behavior);
Registry.register('my-service', ref);

// Lookup elsewhere in the application
const service = Registry.lookup('my-service');
await GenServer.call(service, 'ping');

// Non-throwing variant
const maybeService = Registry.whereis('optional-service');
if (maybeService) {
  GenServer.cast(maybeService, 'notify');
}

// Automatic cleanup on termination
await GenServer.stop(ref);
Registry.isRegistered('my-service'); // false

Built-in Services

noex includes production-ready services built on GenServer:

EventBus

Pub/sub messaging with wildcard pattern matching:

import { EventBus } from 'noex';

const bus = await EventBus.start();

// Subscribe with wildcards
await EventBus.subscribe(bus, 'user.*', (message, topic) => {
  console.log(`${topic}: ${JSON.stringify(message)}`);
});

// Publish events
EventBus.publish(bus, 'user.created', { id: '123', name: 'Alice' });
EventBus.publish(bus, 'user.updated', { id: '123', email: '[email protected]' });

await EventBus.stop(bus);

Cache

In-memory cache with TTL and LRU eviction:

import { Cache } from 'noex';

const cache = await Cache.start({
  maxSize: 1000,
  defaultTtlMs: 60000, // 1 minute
});

await Cache.set(cache, 'session:abc', { userId: '123' });
await Cache.set(cache, 'temp', { data: 'expires soon' }, { ttlMs: 5000 });

const session = await Cache.get(cache, 'session:abc');

// Get or compute
const user = await Cache.getOrSet(cache, 'user:123', async () => {
  return await fetchUserFromDatabase('123');
});

const stats = await Cache.stats(cache);
console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(1)}%`);

await Cache.stop(cache);

RateLimiter

Sliding window rate limiting:

import { RateLimiter, RateLimitExceededError } from 'noex';

const limiter = await RateLimiter.start({
  maxRequests: 100,
  windowMs: 60000, // 100 requests per minute
});

// Check without consuming
const status = await RateLimiter.check(limiter, 'user:123');
console.log(`Remaining: ${status.remaining}/${status.limit}`);

// Consume quota
try {
  await RateLimiter.consume(limiter, 'user:123');
  // Process request
} catch (e) {
  if (e instanceof RateLimitExceededError) {
    console.log(`Rate limited. Retry after ${e.retryAfterMs}ms`);
  }
}

await RateLimiter.stop(limiter);

Observer

The Observer module provides real-time introspection into your supervision tree:

import { Observer } from 'noex';

// Get a snapshot of all processes
const snapshot = Observer.getSnapshot();
console.log(`Total processes: ${snapshot.processCount}`);
console.log(`Total messages processed: ${snapshot.totalMessages}`);

// Iterate GenServer statistics
for (const server of snapshot.servers) {
  console.log(`${server.id}: ${server.messageCount} messages, queue: ${server.queueSize}`);
}

// Get hierarchical process tree
const tree = Observer.getProcessTree();
// Returns nested structure of supervisors and their children

// Subscribe to real-time events
const unsubscribe = Observer.subscribe((event) => {
  console.log(`Event: ${event.type}`, event);
});

// Start polling for periodic updates
const stopPolling = Observer.startPolling(1000, (event) => {
  if (event.type === 'stats_update') {
    console.log('Stats updated');
  }
});

Dashboard (TUI)

noex includes an optional TUI dashboard for real-time monitoring. It requires blessed-contrib:

npm install blessed-contrib
import { Dashboard } from 'noex/dashboard';

const dashboard = new Dashboard({
  refreshInterval: 500,  // Update every 500ms
  theme: 'dark',         // 'dark' or 'light'
  layout: 'full',        // 'full', 'compact', or 'minimal'
});

dashboard.start();

// The dashboard displays:
// - Process tree with status indicators
// - GenServer statistics table
// - Memory usage gauge
// - Real-time event log

// Keyboard controls:
// q, Escape  - Quit
// r          - Refresh
// 1/2/3      - Switch layouts
// Tab        - Navigate widgets
// Enter      - Process details
// ?          - Help

Dashboard Layouts

| Layout | Widgets | |--------|---------| | full | Process tree, stats table, memory gauge, event log | | compact | Process tree, stats table | | minimal | Stats table only |

Dashboard API

| Method | Description | |--------|-------------| | start() | Start the dashboard | | stop() | Stop and cleanup | | refresh() | Force immediate refresh | | switchLayout(layout) | Change layout at runtime | | selectProcess(id) | Show process details | | isRunning() | Check if dashboard is active | | getLayout() | Get current layout |

Lifecycle Events

Monitor your services with lifecycle events:

import { GenServer, Supervisor } from 'noex';

GenServer.onLifecycleEvent((event) => {
  switch (event.type) {
    case 'started':
      console.log(`Server started: ${event.ref.id}`);
      break;
    case 'terminated':
      console.log(`Server terminated: ${event.ref.id} (${event.reason})`);
      break;
    case 'crashed':
      console.error(`Server crashed: ${event.ref.id}`, event.error);
      break;
  }
});

Supervisor.onLifecycleEvent((event) => {
  if (event.type === 'restarted') {
    console.log(`Child restarted (attempt ${event.attempt})`);
  }
});

Error Handling

noex provides specific error types for different failure scenarios:

import {
  CallTimeoutError,
  ServerNotRunningError,
  InitializationError,
  MaxRestartsExceededError,
  DuplicateChildError,
  ChildNotFoundError,
  AlreadyRegisteredError,
  NotRegisteredError,
} from 'noex';

try {
  await GenServer.call(ref, 'message', { timeout: 1000 });
} catch (e) {
  if (e instanceof CallTimeoutError) {
    console.log(`Call timed out after ${e.timeoutMs}ms`);
  } else if (e instanceof ServerNotRunningError) {
    console.log(`Server ${e.serverId} is not running`);
  }
}

API Reference

GenServer

| Method | Description | |--------|-------------| | start(behavior, options?) | Start a new GenServer | | call(ref, message, options?) | Synchronous request/response | | cast(ref, message) | Asynchronous fire-and-forget | | stop(ref, reason?) | Graceful shutdown | | isRunning(ref) | Check if server is running | | onLifecycleEvent(handler) | Register lifecycle handler |

Supervisor

| Method | Description | |--------|-------------| | start(options?) | Start a new Supervisor | | stop(ref, reason?) | Graceful shutdown | | startChild(ref, spec) | Dynamically add a child | | terminateChild(ref, id) | Stop a specific child | | restartChild(ref, id) | Manually restart a child | | getChildren(ref) | Get all children info | | getChild(ref, id) | Get specific child info | | countChildren(ref) | Get child count | | isRunning(ref) | Check if supervisor is running | | onLifecycleEvent(handler) | Register lifecycle handler |

Registry

| Method | Description | |--------|-------------| | register(name, ref) | Register a process by name | | lookup(name) | Look up by name (throws if not found) | | whereis(name) | Look up by name (returns undefined) | | unregister(name) | Remove registration | | isRegistered(name) | Check if name is registered | | getNames() | Get all registered names | | count() | Get registration count |

Distribution (Experimental)

noex includes a distribution layer for building multi-node actor systems. It enables transparent communication between GenServers running on different machines — same API, no code changes.

Status: Experimental. The distribution module is functional for prototyping and small clusters (2-5 nodes) but has not been battle-tested in production. See Known Limitations below.

import { Cluster, GenServer } from 'noex';

// Start a cluster node
await Cluster.start({
  nodeId: '[email protected]:4369',
  seeds: ['[email protected]:4369'],
  clusterSecret: 'my-secret', // HMAC-SHA256 authentication
});

// Call a remote GenServer — same API as local
const remoteRef = { id: 'counter', nodeId: '[email protected]:4369' };
const value = await GenServer.call(remoteRef, { type: 'get' });

Distribution Features

| Feature | Description | |---------|-------------| | Cluster | Seed-based discovery with gossip protocol | | RemoteCall | Transparent RPC across nodes | | RemoteSpawn | Start processes on remote nodes | | DistributedSupervisor | Supervision trees spanning multiple nodes | | GlobalRegistry | Cluster-wide process discovery | | RemoteMonitor / RemoteLink | Cross-node monitoring and linking |

Transport

  • TCP with length-prefix framing
  • HMAC-SHA256 message authentication
  • Heartbeat-based failure detection (configurable interval + threshold)
  • Exponential backoff with jitter for reconnection
  • JSON serialization with support for Date, Map, Set, Error, BigInt, RegExp

Known Limitations

Core (Stable)

The core module (GenServer, Supervisor, Registry, services) is stable and well-tested. Known considerations:

  • Single-threaded: Like all Node.js, each noex instance runs on a single event loop. CPU-bound work in a GenServer blocks the entire process. Use worker_threads or offload heavy computation.
  • In-memory state: GenServer state lives in memory. Use the persistence module or external storage for durability.

Distribution (Experimental)

The distribution layer is functional but has limitations you should be aware of before using it:

  • No split-brain detection: If a network partition divides your cluster, both sides operate independently. After the partition heals, there is no automatic reconciliation — GlobalRegistry entries may conflict. This is the same behavior as Erlang/OTP, but it means you should not rely on GlobalRegistry for strong consistency.
  • No backpressure: The transport layer does not monitor socket write buffers. Under sustained high throughput, memory usage may grow unbounded. Not suitable for high-throughput messaging (>10K msg/sec) without external flow control.
  • Cast messages can be silently dropped: GenServer.cast() to a disconnected remote node is silently discarded (fire-and-forget semantics). Use GenServer.call() when you need delivery confirmation.
  • No encryption: Messages are authenticated (HMAC-SHA256) but not encrypted. Use a VPN or TLS tunnel for sensitive data in transit.
  • No circuit breakers: Failed remote calls will keep attempting until the heartbeat timeout detects the node as down (default: ~15 seconds). Applications should implement their own circuit breaker or retry logic.
  • Limited serialization: Functions, class instances (prototypes), Symbols, and circular references cannot be serialized across nodes.
  • Single connection per peer: Each node pair uses one TCP connection, which may become a bottleneck in high-fanout scenarios.
  • No integration/chaos tests: The distribution module has unit tests but lacks multi-process integration tests and network partition tests. It has not been validated under adversarial network conditions.

What This Means in Practice

| Use Case | Recommendation | |----------|----------------| | Single-node services | Use with confidence. Core is stable and well-tested. | | Prototyping distributed systems | Good to go. Distribution works well for exploring multi-node patterns. | | Small internal clusters (2-5 nodes, reliable network) | Viable with caution. Monitor for the limitations above. | | Production with unreliable networks | Not recommended yet. Needs split-brain detection and backpressure handling. | | High-throughput messaging (>10K msg/sec) | Not recommended yet. Needs backpressure and connection pooling. |

Support

If you find noex useful, consider supporting its development:

Sponsor

License

MIT