@fimbul-works/observable
v2.2.0
Published
A lightweight, strongly-typed TypeScript library for reactive programming patterns, providing observable collections, values, and event handling.
Maintainers
Readme
@fimbul-works/observable
A lightweight, type-safe Observable library for TypeScript that provides reactive programming primitives with strong typing support.
Features
- 🎯 Fully type-safe with TypeScript
- 🪶 Lightweight with zero dependencies
- 🏃♂️ High-performance implementation
- 🧩 Modular design with multiple observable patterns
- ⏱️ Full async support with Promise-based APIs
Installation
npm install @fimbul-works/observableor
yarn add @fimbul-works/observableUsage
The library provides several observable patterns:
Signal
A low-level primitive for implementing publish/subscribe patterns with error handling and async support.
Example:
import { Signal } from '@fimbul-works/observable';
const signal = new Signal<string>();
// Connect handler with automatic cleanup
const cleanup = signal.connect((message) => {
console.log(`Received: ${message}`);
});
// One-time handler
signal.once((message) => {
console.log(`Received once: ${message}`);
});
// Error handling
const errorCleanup = signal.connectError((error) => {
console.error('Handler error:', error);
});
// Synchronous emit (doesn't wait for async handlers)
signal.emit('Hello!');
// Async emit (waits for all handlers, including promises)
await signal.emitAsync('Hello with waiting!');
// Cleanup handlers and resources when done
signal.destroy();
// Or remove specific listeners
cleanup();
errorCleanup();EventEmitter
A strongly-typed event emitter for handling multiple event types with async support.
import { EventEmitter } from '@fimbul-works/observable';
// Define your event types
interface AppEvents {
userLogin: { userId: string, timestamp: number };
error: Error;
notify: string;
}
// Create an event emitter with typed events
const events = new EventEmitter<AppEvents>();
// Subscribe with cleanup function
const cleanup = events.on('userLogin', async ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
await saveLoginToDatabase(userId, timestamp);
});
const errorCleanup = events.onError('userLogin', (error) => {
console.error('Error in login handler:', error);
});
// Emit events synchronously (doesn't wait for async handlers)
events.emit('userLogin', { userId: 'alice', timestamp: Date.now() });
// Emit events and wait for all handlers to complete
await events.emitAsync('userLogin', { userId: 'bob', timestamp: Date.now() });
// Cleanup when done
cleanup();
errorCleanup();ObservableValue
A simple value container that notifies observers when the value changes, with async support.
import { ObservableValue } from '@fimbul-works/observable';
const counter = new ObservableValue(0);
// Subscribe to changes
const unsubscribe = counter.onChange((value) => {
console.log(`Counter changed to: ${value}`);
});
// Update synchronously
counter.set(1); // Logs: "Counter changed to: 1"
// Update with an async transformation
await counter.updateAsync(value => value + 1); // Waits for all handlers
// Cleanup when done
unsubscribe();ObservableMap
A Map implementation that emits events when entries are added, updated, or removed, with async support.
import { ObservableMap } from '@fimbul-works/observable';
const users = new ObservableMap<string, User>();
users.onChange(async (event) => {
switch (event.type) {
case 'add':
console.log(`Added user: ${event.value.name}`);
await saveUserToDatabase(event.value);
break;
case 'update':
console.log(`Updated user: ${event.value.name}`);
await updateUserInDatabase(event.value);
break;
case 'delete':
console.log(`Deleted user: ${event.key}`);
await deleteUserFromDatabase(event.key);
break;
}
});
// Synchronous operations (don't wait for async handlers)
users.set('user1', { id: 1, name: 'Alice' });
users.delete('user1');
// Asynchronous operations (wait for all handlers to complete)
await users.setAsync('user2', { id: 2, name: 'Bob' });
await users.deleteAsync('user2');
await users.clearAsync();ObservableSet
A Set implementation that notifies observers of additions and removals, with async support.
import { ObservableSet } from '@fimbul-works/observable';
const activeUsers = new ObservableSet<string>();
activeUsers.onChange(async (event) => {
switch (event.type) {
case 'add':
console.log(`User became active: ${event.key}`);
await updateUserStatus(event.key, 'active');
break;
case 'delete':
console.log(`User became inactive: ${event.key}`);
await updateUserStatus(event.key, 'inactive');
break;
}
});
// Synchronous operations
activeUsers.add('alice');
// Asynchronous operations (wait for all handlers)
await activeUsers.addAsync('bob');
await activeUsers.deleteAsync('alice');
await activeUsers.clearAsync();ObservableRegistry
A stricter version of ObservableMap that enforces unique registration and required existence, with async support.
import { ObservableRegistry } from '@fimbul-works/observable';
const plugins = new ObservableRegistry<string, Plugin>();
// Will throw if 'logger' is already registered
plugins.register('logger', new LoggerPlugin());
// Async registration (waits for all change handlers)
await plugins.registerAsync('database', new DatabasePlugin());
// Will throw if 'unknown' is not registered
const logger = plugins.get('logger');
// Update a registered value
plugins.update('logger', new EnhancedLoggerPlugin());
// Update with async handlers
await plugins.updateAsync('database', new OptimizedDatabasePlugin());
// Update using a transformation function
plugins.updateWith('logger', (currentPlugin) => {
currentPlugin.level = 'debug';
return currentPlugin;
});
// Async transformation (waits for all handlers)
await plugins.updateWithAsync('database', async (db) => {
await db.optimize();
return db;
});API Documentation
ObservableValue
constructor(initial: T): Creates a new observable valueget(): T: Returns the current valueset(newValue: T): this: Updates the value and notifies observerssetAsync(newValue: T): Promise<this>: Updates the value and waits for all observersupdate(updateFn: (current: T) => T): this: Updates the value using a transform functionupdateAsync(updateFn: (current: T) => T): Promise<this>: Updates with a transform and waits for all observerssubscribe(fn: (value: T) => void): () => void: Immediately calls with current value and subscribes to changesonChange(fn: (value: T) => void): () => void: Subscribes to value changes and returns cleanup function
ObservableMap<K, V>
set(key: K, value: V): this: Sets a value for a keysetAsync(key: K, value: V): Promise<this>: Sets a value and waits for all handlersget(key: K): V | undefined: Gets a value by keydelete(key: K): boolean: Removes a key-value pairdeleteAsync(key: K): Promise<boolean>: Removes a key-value pair and waits for all handlersclear(): void: Removes all entriesclearAsync(): Promise<void>: Removes all entries and waits for all handlershas(key: K): boolean: Checks if a key existssize: number: Number of entries in the maponChange(fn: (event: CollectionEvent<K, V>) => void): () => void: Subscribes to changes
ObservableSet
add(value: T): this: Adds a value to the setaddAsync(value: T): Promise<this>: Adds a value and waits for all handlersdelete(value: T): boolean: Removes a valuedeleteAsync(value: T): Promise<boolean>: Removes a value and waits for all handlershas(value: T): boolean: Checks if a value existsclear(): void: Removes all valuesclearAsync(): Promise<void>: Removes all values and waits for all handlerssize: number: Number of values in the setvalues(): IterableIterator<T>: Returns an iterator of valuesonChange(fn: (event: CollectionEvent<T, boolean>) => void): () => void: Subscribes to changes
ObservableRegistry<K, V>
Extends ObservableMap with:
register(key: K, value: V): this: Registers a new key-value pair (throws if key exists)registerAsync(key: K, value: V): Promise<this>: Registers a key-value pair and waits for all handlersunregister(key: K): boolean: Removes a registrationunregisterAsync(key: K): Promise<boolean>: Removes a registration and waits for all handlersget(key: K, throwErrorOnMissing = true): V | undefined: Gets a value (throws if key doesn't exist and throwErrorOnMissing is true)update(key: K, value: V): this: Updates an existing key (throws if key doesn't exist)updateAsync(key: K, value: V): Promise<this>: Updates an existing key and waits for all handlersupdateWith(key: K, updateFn: (currentValue: V) => V): this: Updates using a transform functionupdateWithAsync(key: K, updateFn: (currentValue: V) => V): Promise<this>: Updates with transform and waits for all handlers
EventEmitter
on<K extends keyof EventMap>(event: K, fn: (data: EventMap[K]) => void | Promise<void>): () => void: Subscribes to an eventoff<K extends keyof EventMap>(event: K, fn: (data: EventMap[K]) => void | Promise<void>): void: Unsubscribes from an eventemit<K extends keyof EventMap>(event: K, data?: EventMap[K]): this: Emits an event synchronouslyemitAsync<K extends keyof EventMap>(event: K, data?: EventMap[K]): Promise<this>: Emits an event and waits for all handlersonError<K extends keyof EventMap>(event: K, fn: (error: Error) => void): () => void: Handles errors for an eventoffError<K extends keyof EventMap>(event: K, fn: (error: Error) => void): this: Removes error handlergetEvents(): Array<keyof EventMap>: Returns all registered event namesdestroy(): void: Cleans up all subscriptions
Signal
connect(fn: (data: T) => void | Promise<void>): () => void: Adds an event handler and returns cleanup functiononce(fn: (data: T) => void | Promise<void>): () => void: Adds a one-time event handlerdisconnect(fn?: (data: T) => void | Promise<void>): this: Removes specific handler or all handlersemit(data: T): number: Emits data to all handlers synchronouslyemitAsync(data: T): Promise<number>: Emits data and waits for all handlers (including promises)connectError(fn: (error: Error) => void): () => void: Adds error handlerdisconnectError(fn: (error: Error) => void): this: Removes error handlerhasHandlers(): boolean: Checks if there are any active handlerslistenerCount(): number: Returns the total number of handlersdestroy(): void: Cleans up all subscriptions and releases resources
What's New in v2.1.0
- Comprehensive Async Support: Added Promise-based async variants for all core operations.
- Enhanced EventEmitter: Now properly handles and awaits async event handlers.
- Improved Signal: Added
emitAsyncmethod that waits for all handlers to complete. - Collection Updates: ObservableMap, ObservableSet, and ObservableRegistry now support async operations.
- ObservableValue Enhancements: Added
setAsyncandupdateAsyncmethods.
Breaking Changes in v2.0.0
- EventEmitter Changes: The
EventEmitterconstructor no longer accepts an events array. Events are now dynamically registered when handlers are attached usingon()oronError(). - Type Safety: The
EventEmitterclass now provides stricter type safety for event data while allowing more flexible usage patterns. - Signal Enhancements: Added a
destroy()method to properly clean up resources.
License
MIT License - See LICENSE file for details.
Built with ⚡ by FimbulWorks
