0.0.1 โข Published 8 months ago
@ffsm/pubsub v0.0.1
@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/pubsubor
yarn add @ffsm/pubsubTable of Contents
- Basic PubSub Usage
- PubSub API Reference
- Type-Safe PubSub Usage
- Browser Integration
- State Management with StoreBase
- Advanced Store Patterns
- Best Practices
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 classcreateId(): Generates a unique subscription ID
Instance Methods
register(channelName: string): Registers a channel if it doesn't existsub<Data, Value, NativeEvent>(channelName: string, eventName: string, handler: Function, id?: string): Subscribes to an eventunsub(channelName: string, eventName: string, id: string): Unsubscribes from an eventunsubAll(channelName?: string, eventName?: string): Unsubscribes from all events, a channel, or an event in a channelpub<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 stateget state(): Get the current statedispatch(name: string, state: State): Update state and publish eventsubscribe<Value, NativeEvent>(name: string, handler: StoreEventHandler<State, Value, NativeEvent>): Subscribe to store eventsunsubscribe(subId: string): Unsubscribe from a specific subscriptionunsubscribeAll(): 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
- Organize by domain: Create channels based on logical domains or features in your application
- Be consistent with naming: Use a consistent naming convention for events (e.g., past tense for completed actions)
- Document your events: Maintain documentation about what events exist and what data they contain
- Clean up subscriptions: Always unsubscribe when components unmount to prevent memory leaks
- Type your data: Use TypeScript generics to ensure type safety for your event data
- Immutable state: Always create new state objects in your store methods rather than mutating existing state
- Single responsibility: Each store should manage a specific domain of your application
- Event constants: Define event names as constants to avoid typos and improve maintainability
License
MIT
0.0.1
8 months ago