emitnlog
Version:
Emit n' Log: a modern, type-safe library for logging, event notifications, and observability in JavaScript/TypeScript apps.
611 lines (449 loc) • 16.6 kB
Markdown
# Event Notifier Documentation
A simple way to implement observable patterns. Listeners only get notified when something happens — and only if they're subscribed.
## Table of Contents
- [Basic Usage](#basic-usage)
- [Lazy Notifications](#lazy-notifications)
- [Promise-based Event Waiting](#promise-based-event-waiting)
- [Debounced Notifications](#debounced-notifications)
- [Event Notifier Options](#event-notifier-options)
- [Event Mapping and Filtering](#event-mapping-and-filtering)
- [Advanced Usage](#advanced-usage)
- [Best Practices](#best-practices)
## Basic Usage
Create an event notifier, subscribe to events, and emit notifications:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
const subscription = notifier.onEvent((msg) => {
console.log(`Received: ${msg}`);
});
notifier.notify('Hello!');
subscription.close();
```
### Multiple Listeners
You can have multiple listeners for the same event notifier:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<{ user: string; action: string }>();
// First listener for logging
const logSubscription = notifier.onEvent(({ user, action }) => {
console.log(`User ${user} performed: ${action}`);
});
// Second listener for analytics
const analyticsSubscription = notifier.onEvent(({ user, action }) => {
analytics.track('user_action', { user, action });
});
// Notify all listeners
notifier.notify({ user: 'Alice', action: 'login' });
// Clean up subscriptions
logSubscription.close();
analyticsSubscription.close();
```
## Lazy Notifications
Notifications support lazy evaluation - the notification function is only called if there are active listeners:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// No listeners yet, this won't execute the function
notifier.notify(() => {
console.log('This is never executed because no listeners');
return 'Hello world';
});
// Now add a listener
const subscription = notifier.onEvent((message) => console.log(message));
// This will execute the function since we have a listener
notifier.notify(() => {
console.log('This runs only when someone is listening');
return 'Hello again!';
});
// Clean up
subscription.close();
```
### Lazy Evaluation Benefits
Lazy evaluation is particularly useful for expensive operations:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const progressNotifier = createEventNotifier<{ progress: number; details: string }>();
// Expensive operation that only runs when needed
progressNotifier.notify(() => {
const expensiveDetails = generateDetailedProgressReport(); // Only called if listeners exist
return { progress: 75, details: expensiveDetails };
});
```
## Promise-based Event Waiting
Use `waitForEvent()` to get a Promise that resolves when the next event occurs, without interfering with subscribed listeners. The promise only rejects if the notifier is closed before the next event is emitted - in this case it rejects with a `ClosedError`.
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// Somewhere in an async function
async function handleNextEvent() {
// This will wait until the next event is notified
const eventData = await notifier.waitForEvent();
console.log(`Received event: ${eventData}`);
}
// Wait for multiple events sequentially
async function handleMultipleEvents() {
// These will wait for two separate events in sequence
const first = await notifier.waitForEvent();
const second = await notifier.waitForEvent();
console.log(`Got two events: ${first}, ${second}`);
}
```
### Important Note About Concurrent Waiting
Multiple concurrent `waitForEvent()` calls will all resolve with the same event:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// Caution: This doesn't wait for two separate events!
// Both promises resolve with the same event
async function incorrectUsage() {
const [event1, event2] = await Promise.all([notifier.waitForEvent(), notifier.waitForEvent()]);
// event1 and event2 will be identical
}
```
### Combining Subscriptions and Waiting
You can combine regular subscriptions with promise-based waiting:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const statusNotifier = createEventNotifier<'connecting' | 'connected' | 'disconnected'>();
// Regular subscription for logging
const logSubscription = statusNotifier.onEvent((status) => {
console.log(`Status changed to: ${status}`);
});
// Wait for connection
async function waitForConnection() {
let status = await statusNotifier.waitForEvent();
while (status !== 'connected') {
status = await statusNotifier.waitForEvent();
}
console.log('Successfully connected!');
}
// Both the subscription and the waiting will receive events
statusNotifier.notify('connecting');
statusNotifier.notify('connected');
### Closing Behavior
- `waitForEvent()` will reject with an error if `close()` is called before the next event occurs.
- After closing, you can still call `waitForEvent()` again; a new internal waiter will be created and the next `notify()` will resolve it.
### Lazy Evaluation Nuance
If you call `notify()` with a function, it will only be executed when there are active listeners or a pending waiter created by `waitForEvent()`. This ensures lazy computations still happen when someone is awaiting the next event, even if no listeners are registered.
```
## Debounced Notifications
The notifier can be created with a debounced delay for scenarios where events are notified too quickly:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
// Create a debounced notifier with 300ms delay
const debouncedNotifier = createEventNotifier<string>({ debounceDelay: 300 });
const subscription = debouncedNotifier.onEvent((msg) => {
console.log(`Debounced message: ${msg}`);
});
// Rapid notifications - only the last one will be emitted
debouncedNotifier.notify('First message');
debouncedNotifier.notify('Second message');
debouncedNotifier.notify('Third message');
// After 300ms: Only "Third message" will be logged
subscription.close();
```
### Debounced Notifications with Lazy Evaluation
Debouncing works seamlessly with lazy evaluation:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const searchNotifier = createEventNotifier<string>({ debounceDelay: 500 });
searchNotifier.onEvent((query) => {
// This will only be called after 500ms of inactivity
performSearch(query);
});
// Simulate rapid typing
searchNotifier.notify(() => {
console.log('Computing search for "h"'); // Won't execute
return 'h';
});
searchNotifier.notify(() => {
console.log('Computing search for "he"'); // Won't execute
return 'he';
});
searchNotifier.notify(() => {
console.log('Computing search for "hello"'); // Will execute after 500ms
return 'hello';
});
```
## Event Notifier Options
When creating an event notifier, you can provide options:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
interface EventNotifierOptions {
debounceDelay?: number; // Debounce delay in milliseconds
}
const notifier = createEventNotifier<string>({ debounceDelay: 100 });
```
### Error Handling
You can register an error handler to catch errors that occur in listeners:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// Register error handler
notifier.onError((error) => {
console.error('Listener error:', error);
// Send to error tracking service
errorTracker.captureException(error);
});
// Add a listener that might throw
notifier.onEvent((msg) => {
if (msg === 'bad') {
throw new Error('Bad message received');
}
console.log(msg);
});
notifier.notify('hello'); // Works fine
notifier.notify('bad'); // Error caught by error handler
```
## Event Mapping and Filtering
The `mapOnEvent` function allows you to transform and filter events from existing notifiers, creating new event streams with different types or conditional logic.
### Basic Event Mapping
```ts
import { createEventNotifier, mapOnEvent } from 'emitnlog/notifier';
// Original notifier with raw storage events
const storageNotifier = createEventNotifier<{ action: string; data: unknown }>();
// Map to user-specific events
const userEvents = mapOnEvent(storageNotifier.onEvent, (event) => {
if (event.action === 'user_created') {
return { type: 'user_created' as const, userId: event.data.id, timestamp: new Date() };
}
if (event.action === 'user_updated') {
return { type: 'user_updated' as const, userId: event.data.id, changes: event.data.changes };
}
// Skip other events
return SKIP_MAPPED_EVENT;
});
// Subscribe to the mapped events
const subscription = userEvents((userEvent) => {
console.log(`User event: ${userEvent.type} for user ${userEvent.userId}`);
});
```
### Conditional Event Filtering
Use `SKIP_MAPPED_EVENT` to filter out events you don't want to pass to listeners:
```ts
import { createEventNotifier, mapOnEvent, SKIP_MAPPED_EVENT } from 'emitnlog/notifier';
const systemNotifier = createEventNotifier<{ level: string; message: string }>();
// Only pass through error and warning events
const errorEvents = mapOnEvent(systemNotifier.onEvent, (event) => {
if (event.level === 'error' || event.level === 'warning') {
return event; // Pass through
}
return SKIP_MAPPED_EVENT; // Skip info/debug events
});
errorEvents((event) => {
console.error(`${event.level.toUpperCase()}: ${event.message}`);
});
// These will be passed through
systemNotifier.notify({ level: 'error', message: 'Database connection lost' });
systemNotifier.notify({ level: 'warning', message: 'High memory usage' });
// This will be skipped
systemNotifier.notify({ level: 'info', message: 'User logged in' });
```
### Creating Derived Event Streams
You can create multiple derived event streams from a single source:
```ts
import { createEventNotifier, mapOnEvent, SKIP_MAPPED_EVENT } from 'emitnlog/notifier';
interface ApiEvent {
method: string;
path: string;
status: number;
duration: number;
}
const apiNotifier = createEventNotifier<ApiEvent>();
// Create error-specific events
const errorEvents = mapOnEvent(apiNotifier.onEvent, (event) => {
if (event.status >= 400) {
return { error: true, status: event.status, endpoint: `${event.method} ${event.path}`, duration: event.duration };
}
return SKIP_MAPPED_EVENT;
});
// Create slow request events
const slowRequestEvents = mapOnEvent(apiNotifier.onEvent, (event) => {
if (event.duration > 1000) {
return { slow: true, endpoint: `${event.method} ${event.path}`, duration: event.duration };
}
return SKIP_MAPPED_EVENT;
});
// Subscribe to different event types
errorEvents((event) => {
console.error(`API Error ${event.status}: ${event.endpoint}`);
});
slowRequestEvents((event) => {
console.warn(`Slow request: ${event.endpoint} took ${event.duration}ms`);
});
```
## Advanced Usage
### Typed Event Notifiers
You can create strongly-typed event notifiers for complex data structures:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
interface UserEvent {
userId: string;
action: 'login' | 'logout' | 'update';
timestamp: Date;
metadata?: Record<string, unknown>;
}
const userNotifier = createEventNotifier<UserEvent>();
userNotifier.onEvent((event) => {
// TypeScript knows the exact shape of the event
console.log(`User ${event.userId} performed ${event.action} at ${event.timestamp}`);
if (event.metadata) {
console.log('Additional metadata:', event.metadata);
}
});
userNotifier.notify({ userId: 'user123', action: 'login', timestamp: new Date(), metadata: { source: 'web' } });
```
### Event Processing
You can implement conditional logic in your event handlers:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const errorNotifier = createEventNotifier<Error>();
errorNotifier.onEvent((error) => {
if (error.message.includes('network')) {
handleNetworkError(error);
} else {
handleGenericError(error);
}
});
// You can conditionally call notify based on your logic
function notifyError(error: Error) {
if (error.message.includes('CRITICAL')) {
errorNotifier.notify(error); // Only notify for critical errors
}
// Non-critical errors are simply not notified
}
```
### Event Notifier Cleanup
Always clean up subscriptions to prevent memory leaks:
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
class EventManager {
private subscriptions: Array<{ close(): void }> = [];
addSubscription(handler: (event: string) => void) {
const subscription = notifier.onEvent(handler);
this.subscriptions.push(subscription);
return subscription;
}
cleanup() {
this.subscriptions.forEach((sub) => sub.close());
this.subscriptions = [];
}
}
const manager = new EventManager();
manager.addSubscription((msg) => console.log(msg));
// Later, clean up all subscriptions
manager.cleanup();
```
## Best Practices
### 1. Use Lazy Evaluation for Expensive Operations
```ts
// Good: Expensive operation only runs when needed
notifier.notify(() => {
const expensiveData = computeExpensiveData();
return { data: expensiveData, timestamp: Date.now() };
});
// Less optimal: Always computes expensive data
const expensiveData = computeExpensiveData();
notifier.notify({ data: expensiveData, timestamp: Date.now() });
```
### 2. Clean Up Subscriptions
```ts
// Good: Clean up subscriptions
const subscription = notifier.onEvent(handler);
// ... later
subscription.close();
// Use try-finally or similar patterns for guaranteed cleanup
try {
const subscription = notifier.onEvent(handler);
// ... do work
} finally {
subscription.close();
}
```
### 3. Use Debouncing for Rapid Events
```ts
// Good: Debounce rapid UI events
const searchNotifier = createEventNotifier<string>({ debounceDelay: 300 });
searchNotifier.onEvent((query) => {
performSearch(query); // Only called after user stops typing
});
// Input handler
function onSearchInput(event: InputEvent) {
const query = (event.target as HTMLInputElement).value;
searchNotifier.notify(query);
}
```
### 4. Use Strong Types
```ts
// Good: Strongly typed events
interface AppEvent {
type: 'user_action' | 'system_event';
data: unknown;
timestamp: Date;
}
const appNotifier = createEventNotifier<AppEvent>();
// Less optimal: Weak typing
const weakNotifier = createEventNotifier<any>();
```
### 5. Handle Errors Gracefully
```ts
import { createEventNotifier } from 'emitnlog/notifier';
const notifier = createEventNotifier<string>();
// Good: Handle errors in listeners
notifier.onEvent((msg) => {
try {
processMessage(msg);
} catch (error) {
console.error('Error processing message:', error);
}
});
// Good: Handle errors in lazy notifications
notifier.notify(() => {
try {
return computeComplexData();
} catch (error) {
console.error('Error computing data:', error);
return 'fallback data';
}
});
```
### 6. Use Meaningful Event Types
```ts
// Good: Descriptive event types
interface DatabaseEvent {
operation: 'insert' | 'update' | 'delete';
table: string;
recordId: string;
success: boolean;
}
const dbNotifier = createEventNotifier<DatabaseEvent>();
// Less optimal: Generic events
const genericNotifier = createEventNotifier<{ type: string; data: unknown }>();
```
### 7. Use mapOnEvent for Conditional Notifications
For conditional event filtering and transformation, use `mapOnEvent` rather than complex logic in listeners:
```ts
import { createEventNotifier, mapOnEvent, SKIP_MAPPED_EVENT } from 'emitnlog/notifier';
// Good: Use mapOnEvent for filtering
const rawEvents = createEventNotifier<{ severity: string; message: string }>();
const criticalEvents = mapOnEvent(rawEvents.onEvent, (event) => {
if (event.severity === 'critical') {
return { message: event.message, timestamp: new Date() };
}
return SKIP_MAPPED_EVENT;
});
criticalEvents((event) => {
sendAlert(event.message);
});
// Less optimal: Complex filtering in listeners
rawEvents.onEvent((event) => {
if (event.severity === 'critical') {
sendAlert(event.message);
}
// Other severities are processed but ignored
});
```
---
[← Back to main README](../README.md)