0.0.1 โ€ข Published 8 months ago

@ffsm/pubsub v0.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

@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