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

@vorthain/nexus

v0.2.3

Published

A lightweight, type-safe pub/sub library for JavaScript applications.

Downloads

14

Readme

⚡ @vorthain/nexus

npm Downloads Bundle Size

A lightweight, type-safe pub/sub library for JavaScript applications. Create a central communication hub with strongly-typed events, built-in debugging, and zero dependencies.

Why Nexus?

Building decoupled, maintainable applications often requires a reliable way for components to communicate without direct dependencies. Nexus provides:

  • Type Safety: Define your events upfront and get autocomplete in modern IDEs
  • Debugging Built-in: Toggle debug mode to see all events flowing through your system
  • Lightweight: Zero dependencies
  • Predictable: No wildcard events, no event bubbling, no hidden surprises
  • Battle-tested patterns: Implements proven pub/sub patterns with a clean API

Perfect for:

  • Decoupling components in large applications
  • Managing application-wide events
  • Building plugin systems
  • Creating reactive architectures
  • State synchronization across modules
  • Inter-frame communication

Installation

npm install @vorthain/nexus

Quick Start

import { createNexusHub } from '@vorthain/nexus';

// Define your events upfront
const hub = createNexusHub(['user:login', 'user:logout', 'data:updated', 'modal:open', 'modal:close']);

// Subscribe to events
hub.on('user:login', 'auth-handler', (user) => {
  console.log('User logged in:', user);
});

// Emit events
hub.emit('user:login', { id: 123, name: 'Alice' });

Core Concepts

Event Names Are Contracts

Unlike traditional event emitters, Nexus requires you to define all event names upfront. This provides:

  • Type safety - Your IDE knows all valid events
  • Documentation - Event names serve as API documentation
  • No typos - Invalid event names throw errors immediately
  • Discoverability - Use hub.eventNames() to see all available events

Listener IDs Prevent Duplication

Every listener must have a unique ID per event. This prevents:

  • Accidental duplicate subscriptions
  • Memory leaks from forgotten listeners
  • Makes debugging easier (you know exactly which listener is firing)

API

createNexusHub(eventNames)

Creates a new event hub with the specified event names.

const hub = createNexusHub(['click', 'change', 'submit']);

hub.on(eventName, id, callback)

Subscribe to an event.

hub.on('click', 'button-handler', (data) => {
  console.log('Button clicked:', data);
});

hub.once(eventName, id, callback)

Subscribe to an event once. The listener is automatically removed after the first emission.

hub.once('submit', 'form-validator', (formData) => {
  console.log('Form submitted once:', formData);
});

hub.off(eventName, id?)

Unsubscribe from an event. If no ID is provided, removes all listeners for that event.

// Remove specific listener
hub.off('click', 'button-handler');

// Remove all listeners for an event
hub.off('click');

hub.emit(eventName, data?)

Emit an event to all listeners. Returns true if there were listeners, false otherwise.

const hadListeners = hub.emit('click', { x: 100, y: 200 });
if (!hadListeners) {
  console.log('No one was listening to click event');
}

hub.clear(eventName?)

Clear listeners. If no event name provided, clears all listeners.

// Clear specific event
hub.clear('click');

// Clear all events
hub.clear();

hub.listenerCount(eventName?)

Get the number of listeners.

// Count for specific event
const clickListeners = hub.listenerCount('click');

// Total count across all events
const totalListeners = hub.listenerCount();

hub.listeners(eventName?)

Get array of listener functions.

// Get listeners for specific event
const clickHandlers = hub.listeners('click');

// Get all listeners
const allHandlers = hub.listeners();

hub.eventNames()

Get all registered event names.

const events = hub.eventNames();
// ['click', 'change', 'submit']

hub.setDebug(enabled)

Enable debug mode to log all events.

// Log all events
hub.setDebug(true);

// Log specific events only
hub.setDebug(['click', 'submit']);

// Disable logging
hub.setDebug(false);

hub.setLogger(fn)

Set a custom logger function.

hub.setLogger((eventName, data) => {
  myLogger.log(`Event: ${eventName}`, data);
});

Examples

React

// eventHub.js
import { createNexusHub } from '@vorthain/nexus';

export const appEvents = createNexusHub(['theme:change', 'notification:show', 'notification:hide', 'data:refresh']);
// ThemeToggle.jsx
import { appEvents } from './eventHub';

function ThemeToggle() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const handleThemeChange = (newTheme) => setTheme(newTheme);

    appEvents.on('theme:change', 'theme-toggle-component', handleThemeChange);

    return () => {
      appEvents.off('theme:change', 'theme-toggle-component');
    };
  }, []);

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    appEvents.emit('theme:change', newTheme);
  };

  return <button onClick={toggleTheme}>Current: {theme}</button>;
}

Vue

// eventHub.js
import { createNexusHub } from '@vorthain/nexus';

export const hub = createNexusHub(['cart:add', 'cart:remove', 'cart:clear', 'user:update']);

// Vue plugin
export default {
  install(app) {
    app.config.globalProperties.$hub = hub;
    app.provide('hub', hub);
  },
};
// main.js
import { createApp } from 'vue';
import eventHubPlugin from './eventHub';

const app = createApp(App);
app.use(eventHubPlugin);
app.mount('#app');
<!-- Cart.vue -->
<script setup>
import { inject, onMounted, onUnmounted } from 'vue';

const hub = inject('hub');
const cartItems = ref([]);

const handleCartAdd = (item) => {
  cartItems.value.push(item);
};

onMounted(() => {
  hub.on('cart:add', 'cart-component', handleCartAdd);
});

onUnmounted(() => {
  hub.off('cart:add', 'cart-component');
});
</script>

Plugin System

class PluginSystem {
  constructor() {
    this.hub = createNexusHub([
      'plugin:register',
      'plugin:unregister',
      'plugin:enable',
      'plugin:disable',
      'hook:before-save',
      'hook:after-save',
      'hook:before-delete',
      'hook:after-delete',
    ]);

    this.plugins = new Map();
  }

  register(plugin) {
    this.plugins.set(plugin.name, plugin);

    // Let plugin subscribe to events
    plugin.init(this.hub);

    this.hub.emit('plugin:register', { name: plugin.name });
  }

  async save(data) {
    // Run before-save hooks
    const shouldContinue = this.hub.emit('hook:before-save', data);
    if (!shouldContinue) return;

    // Perform save
    const result = await saveToDatabase(data);

    // Run after-save hooks
    this.hub.emit('hook:after-save', result);

    return result;
  }
}

// Plugin example
class ValidationPlugin {
  constructor() {
    this.name = 'validation';
  }

  init(hub) {
    hub.on('hook:before-save', 'validation-plugin', (data) => {
      if (!this.validate(data)) {
        throw new Error('Validation failed');
      }
    });
  }

  validate(data) {
    // Validation logic
    return data.name && data.email;
  }
}

State Synchronization

class StateManager {
  constructor() {
    this.hub = createNexusHub(['state:change', 'state:sync', 'state:reset']);

    this.state = {};

    // Enable debug in development
    if (process.env.NODE_ENV === 'development') {
      this.hub.setDebug(true);
    }
  }

  set(path, value) {
    const oldValue = this.get(path);

    // Update state
    this.state[path] = value;

    // Notify listeners
    this.hub.emit('state:change', {
      path,
      oldValue,
      newValue: value,
    });
  }

  subscribe(path, id, callback) {
    this.hub.on('state:change', id, (change) => {
      if (change.path === path) {
        callback(change.newValue, change.oldValue);
      }
    });
  }

  // Sync with external source
  syncWith(externalState) {
    const changes = this.diff(this.state, externalState);
    changes.forEach((change) => {
      this.set(change.path, change.value);
    });
    this.hub.emit('state:sync', changes);
  }
}

Testing

import { createNexusHub } from '@vorthain/nexus';

describe('UserService', () => {
  let hub;
  let userService;

  beforeEach(() => {
    hub = createNexusHub(['user:created', 'user:updated', 'user:deleted']);
    userService = new UserService(hub);
  });

  test('emits user:created event when creating user', async () => {
    const listener = jest.fn();
    hub.on('user:created', 'test', listener);

    const user = await userService.create({ name: 'Alice' });

    expect(listener).toHaveBeenCalledWith(
      expect.objectContaining({
        id: expect.any(String),
        name: 'Alice',
      })
    );
  });

  test('handles errors in event listeners', () => {
    hub.on('user:created', 'bad-listener', () => {
      throw new Error('Listener error');
    });

    hub.on('user:created', 'good-listener', jest.fn());

    // Should not throw - errors are caught
    expect(() => {
      userService.create({ name: 'Bob' });
    }).not.toThrow();
  });
});

Debug Mode

Debug mode helps you understand event flow in your application:

// Enable debug for all events
hub.setDebug(true);

// Enable debug for specific events only
hub.setDebug(['user:login', 'data:save']);

// Custom logger
hub.setLogger((eventName, data) => {
  console.group(`[${new Date().toISOString()}] Event: ${eventName}`);
  console.log('Data:', data);
  console.log('Listener count:', hub.listenerCount(eventName));
  console.groupEnd();
});

// Disable debug
hub.setDebug(false);

Best Practices

1. Use Namespaced Event Names

// Good
const hub = createNexusHub(['user:login', 'user:logout', 'user:update', 'cart:add', 'cart:remove']);

// Less clear
const hub = createNexusHub(['login', 'logout', 'update', 'add', 'remove']);

2. Use Descriptive Listener IDs

// Good - clearly identifies the listener
hub.on('data:update', 'header-username-display', updateUsername);
hub.on('data:update', 'sidebar-user-avatar', updateAvatar);

// Bad - unclear IDs
hub.on('data:update', 'listener1', updateUsername);
hub.on('data:update', 'listener2', updateAvatar);

3. Always Clean Up

class Component {
  constructor(hub) {
    this.hub = hub;
    this.id = `component-${Math.random()}`;
  }

  mount() {
    this.hub.on('update', this.id, this.handleUpdate);
  }

  unmount() {
    // Always remove listeners when done
    this.hub.off('update', this.id);
  }
}

4. Handle Errors Gracefully

hub.on('save', 'auto-save', async (data) => {
  try {
    await saveData(data);
  } catch (error) {
    // Handle error appropriately
    console.error('Auto-save failed:', error);
    hub.emit('save:error', error);
  }
});