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

@ffsm/pubsub

v0.0.1

Published

A lightweight, type-safe publish-subscribe pattern implementation with integrated state management for JavaScript/TypeScript applications

Readme

@ffsm/pubsub

A lightweight, type-safe implementation of the Publish-Subscribe pattern for JavaScript/TypeScript applications with integrated state management capabilities.

Features

  • 🔍 Type-safe: Full TypeScript support with generic types for events and data
  • 🧩 Channel-based: Organize your events into logical channels
  • 🔄 Singleton pattern: Easy to use across your application
  • 📡 DOM integration: Automatically dispatches browser CustomEvents for cross-component communication
  • 💾 State management: Integrated StoreBase for building state containers
  • 🪶 Lightweight: No dependencies, small footprint
  • 🧵 Promise support: Handlers can be async

Installation

npm install @ffsm/pubsub

or

yarn add @ffsm/pubsub

Table of Contents

Basic PubSub Usage

import { PubSub } from '@ffsm/pubsub';

// Get the singleton instance
const pubsub = PubSub.getInstance();

// Subscribe to an event
const subscription = pubsub.sub('userChannel', 'login', (event) => {
  console.log('User logged in:', event.detail.data);
});

// Publish an event
pubsub.pub('userChannel', 'login', { userId: '123', username: 'john_doe' });

// Unsubscribe when done
subscription.unsub();

PubSub API Reference

PubSub Class

Static Methods

  • getInstance(): Returns the singleton instance of the PubSub class
  • createId(): Generates a unique subscription ID

Instance Methods

  • register(channelName: string): Registers a channel if it doesn't exist
  • sub<Data, Value, NativeEvent>(channelName: string, eventName: string, handler: Function, id?: string): Subscribes to an event
  • unsub(channelName: string, eventName: string, id: string): Unsubscribes from an event
  • unsubAll(channelName?: string, eventName?: string): Unsubscribes from all events, a channel, or an event in a channel
  • pub<Data, Value, NativeEvent>(channelName: string, eventName: string, data?: Data, target?: PubSubTarget<Value>, e?: NativeEvent): Publishes an event

Type-Safe PubSub Usage

interface UserData {
  userId: string;
  username: string;
  role: 'admin' | 'user';
}

// Type-safe subscription
pubsub.sub<UserData>('userChannel', 'login', (event) => {
  // TypeScript knows the type of event.detail.data
  const { userId, username, role } = event.detail.data;
  console.log(`User ${username} with role ${role} logged in`);
});

// Type-safe publication
pubsub.pub<UserData>('userChannel', 'login', {
  userId: '123',
  username: 'john_doe',
  role: 'admin'
});

Browser Integration

The PubSub implementation automatically dispatches browser CustomEvents when publishing events in browser environments. This allows for cross-framework communication and integration with browser APIs.

// Listen for PubSub events at the window level
window.addEventListener('userChannel:login', (event) => {
  console.log('Custom event received:', event);
});

// Publish a PubSub event
pubsub.pub('userChannel', 'login', { userId: '123' });

State Management with StoreBase

The @ffsm/pubsub package includes a StoreBase class that provides a foundation for implementing state management with PubSub. This allows you to create typed stores that manage state and notify subscribers of changes.

Basic Store Implementation

import { StoreBase } from '@ffsm/pubsub';

// Define your state interface
interface CounterState {
  count: number;
  lastUpdated: Date;
}

// Define event types
const CounterEvents = {
  INCREMENT: 'increment',
  DECREMENT: 'decrement',
  RESET: 'reset'
} as const;

// Create your store class
class CounterStore extends StoreBase<CounterState> {
  constructor() {
    super({
      count: 0,
      lastUpdated: new Date()
    });
  }

  increment() {
    const newState = {
      count: this.state.count + 1,
      lastUpdated: new Date()
    };
    this.dispatch(CounterEvents.INCREMENT, newState);
  }

  decrement() {
    const newState = {
      count: this.state.count - 1,
      lastUpdated: new Date()
    };
    this.dispatch(CounterEvents.DECREMENT, newState);
  }

  reset() {
    const newState = {
      count: 0,
      lastUpdated: new Date()
    };
    this.dispatch(CounterEvents.RESET, newState);
  }
}

// Create a singleton instance
const counterStore = new CounterStore();

// Subscribe to state changes
const subscription = counterStore.subscribe(CounterEvents.INCREMENT, (event) => {
  console.log('Counter incremented:', event.detail.data.count);
});

// Update state
counterStore.increment(); // Logs: "Counter incremented: 1"

// Unsubscribe when done
subscription.unsub();

StoreBase API Reference

Methods

  • constructor(state: State): Initialize a store with the given state
  • get state(): Get the current state
  • dispatch(name: string, state: State): Update state and publish event
  • subscribe<Value, NativeEvent>(name: string, handler: StoreEventHandler<State, Value, NativeEvent>): Subscribe to store events
  • unsubscribe(subId: string): Unsubscribe from a specific subscription
  • unsubscribeAll(): Unsubscribe from all store events

Integration with React

import React, { useEffect, useState } from 'react';
import { CounterStore, CounterEvents } from './CounterStore';

// Create a singleton instance
const counterStore = new CounterStore();

// Custom hook for using the store
function useCounterStore() {
  const [count, setCount] = useState(counterStore.state.count);

  useEffect(() => {
    // Subscribe to all relevant events
    const incrementSub = counterStore.subscribe(CounterEvents.INCREMENT, (event) => {
      setCount(event.detail.data.count);
    });
    
    const decrementSub = counterStore.subscribe(CounterEvents.DECREMENT, (event) => {
      setCount(event.detail.data.count);
    });
    
    const resetSub = counterStore.subscribe(CounterEvents.RESET, (event) => {
      setCount(event.detail.data.count);
    });

    // Clean up
    return () => {
      incrementSub.unsub();
      decrementSub.unsub();
      resetSub.unsub();
    };
  }, []);

  return {
    count,
    increment: () => counterStore.increment(),
    decrement: () => counterStore.decrement(),
    reset: () => counterStore.reset()
  };
}

// In your component
function Counter() {
  const { count, increment, decrement, reset } = useCounterStore();
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Integration with Vue

import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import { counterStore, CounterEvents } from './CounterStore';

export default defineComponent({
  setup() {
    const count = ref(counterStore.state.count);
    let subscriptions = [];

    onMounted(() => {
      // Subscribe to events
      subscriptions.push(
        counterStore.subscribe(CounterEvents.INCREMENT, (event) => {
          count.value = event.detail.data.count;
        }),
        counterStore.subscribe(CounterEvents.DECREMENT, (event) => {
          count.value = event.detail.data.count;
        }),
        counterStore.subscribe(CounterEvents.RESET, (event) => {
          count.value = event.detail.data.count;
        })
      );
    });

    onUnmounted(() => {
      // Clean up subscriptions
      subscriptions.forEach(sub => sub.unsub());
    });

    return {
      count,
      increment: () => counterStore.increment(),
      decrement: () => counterStore.decrement(),
      reset: () => counterStore.reset()
    };
  }
});

Advanced Store Patterns

Combining Multiple Stores

You can create complex applications by combining multiple stores:

// User store manages authentication
class UserStore extends StoreBase<UserState> {
  // Implementation...
}

// Cart store manages shopping cart
class CartStore extends StoreBase<CartState> {
  // Implementation...
}

// Order store manages order processing
class OrderStore extends StoreBase<OrderState> {
  // Implementation...
}

// Create singleton instances
const userStore = new UserStore();
const cartStore = new CartStore();
const orderStore = new OrderStore();

// Stores can listen to each other's events
userStore.subscribe('logout', () => {
  // Clear cart when user logs out
  cartStore.clearCart();
});

cartStore.subscribe('checkout', (event) => {
  // Create an order when cart checks out
  orderStore.createOrder(event.detail.data.items);
});

Advanced PubSub Example with React Components

import React, { useEffect } from 'react';
import { PubSub } from '@ffsm/pubsub';

const UserProfile = () => {
  useEffect(() => {
    const pubsub = PubSub.getInstance();
    
    // Subscribe to profile updates
    const { unsub } = pubsub.sub('userChannel', 'profileUpdate', (event) => {
      const userData = event.detail.data;
      console.log('Profile updated:', userData);
    });
    
    // Clean up subscription when component unmounts
    return () => {
      unsub();
    };
  }, []);
  
  const handleUpdateClick = () => {
    const pubsub = PubSub.getInstance();
    pubsub.pub('userChannel', 'profileUpdate', { 
      name: 'John Doe',
      avatar: 'https://example.com/avatar.jpg'
    });
  };
  
  return (
    <div>
      <h2>User Profile</h2>
      <button onClick={handleUpdateClick}>Update Profile</button>
    </div>
  );
};

Async Handlers

// Using async handlers
pubsub.sub('dataChannel', 'fetch', async (event) => {
  try {
    const response = await fetch(`https://api.example.com/data/${event.detail.data.id}`);
    const data = await response.json();
    
    // Publish the result to another channel
    pubsub.pub('dataChannel', 'fetchComplete', data);
  } catch (error) {
    pubsub.pub('dataChannel', 'fetchError', { error: error.message });
  }
});

Type-Safe Store with TypeScript

interface TodoItem {
  id: string;
  text: string;
  completed: boolean;
}

interface TodoState {
  items: TodoItem[];
  filter: 'all' | 'active' | 'completed';
}

class TodoStore extends StoreBase<TodoState> {
  constructor() {
    super({
      items: [],
      filter: 'all'
    });
  }
  
  // Type-safe methods with proper state typing
  addTodo(text: string) {
    const newState = {
      ...this.state,
      items: [
        ...this.state.items,
        {
          id: Math.random().toString(36).substring(2),
          text,
          completed: false
        }
      ]
    };
    
    this.dispatch('add', newState);
  }
  
  toggleTodo(id: string) {
    const newState = {
      ...this.state,
      items: this.state.items.map(item => 
        item.id === id 
          ? { ...item, completed: !item.completed } 
          : item
      )
    };
    
    this.dispatch('toggle', newState);
  }
  
  setFilter(filter: TodoState['filter']) {
    const newState = {
      ...this.state,
      filter
    };
    
    this.dispatch('filter', newState);
  }
}

Best Practices

  1. Organize by domain: Create channels based on logical domains or features in your application
  2. Be consistent with naming: Use a consistent naming convention for events (e.g., past tense for completed actions)
  3. Document your events: Maintain documentation about what events exist and what data they contain
  4. Clean up subscriptions: Always unsubscribe when components unmount to prevent memory leaks
  5. Type your data: Use TypeScript generics to ensure type safety for your event data
  6. Immutable state: Always create new state objects in your store methods rather than mutating existing state
  7. Single responsibility: Each store should manage a specific domain of your application
  8. Event constants: Define event names as constants to avoid typos and improve maintainability

License

MIT