@oxog/pulse
v1.0.0
Published
Reactive signals & streams library with TC39 compliance, unified API, and micro-kernel plugin architecture - part of @oxog ecosystem
Downloads
31
Maintainers
Readme
@oxog/pulse
⚡ A modern, high-performance reactive state management library with TC39 Signals compliance
Part of the @oxog ecosystem: @oxog/plugin · @oxog/emitter
✨ Features
- 🎯 TC39 Signals Compliant - Future-proof API following the upcoming standard
- ⚡ Zero Dependencies - Tiny bundle size, maximum performance
- 🔄 Automatic Dependency Tracking - No need to manually declare dependencies
- 📦 Built-in Streams - RxJS-compatible reactive streams with 30+ operators
- 🎨 Framework Agnostic - Works with React, Vue, Svelte, or vanilla JS
- 📦 Batch Updates - Automatic batching for optimal performance
- 🔌 Plugin Architecture - Extensible via @oxog/plugin micro-kernel
- 💪 Full TypeScript - Complete type safety out of the box
📦 Installation
# npm
npm install @oxog/pulse
# yarn
yarn add @oxog/pulse
# pnpm
pnpm add @oxog/pulse🚀 Quick Start
Signals
import { signal, computed, effect } from '@oxog/pulse';
// Create a reactive signal
const count = signal(0);
// Create a computed value (auto-updates when dependencies change)
const doubled = computed(() => count.get() * 2);
// Create an effect (auto-runs when dependencies change)
const dispose = effect(() => {
console.log(`Count: ${count.get()}, Doubled: ${doubled.get()}`);
});
// Logs: "Count: 0, Doubled: 0"
// Update the signal
count.set(5);
// Logs: "Count: 5, Doubled: 10"
// Cleanup when done
dispose();Streams
import { stream, pipe } from '@oxog/pulse';
import { debounce, filter, map } from '@oxog/pulse/operators';
// Create a stream
const search$ = stream<string>();
// Apply operators
const results$ = pipe(
search$,
filter(q => q.length > 2),
debounce(300),
map(q => fetch(`/api/search?q=${q}`))
);
// Subscribe to results
results$.subscribe(result => console.log(result));
// Emit values
search$.next('hel');
search$.next('hello'); // Only this one passes through📖 API Reference
Signals
| Function | Description |
|----------|-------------|
| signal(value, options?) | Create a reactive signal |
| computed(fn, options?) | Create a derived computation |
| effect(fn) | Create a side effect |
| untracked(fn) | Read without dependency tracking |
| batch(fn) | Group updates into single propagation |
Signal Methods
const count = signal(0);
count.get(); // Read value (tracks dependency)
count.set(5); // Set value
count.value; // Property accessor (get/set)
count.update(n => n + 1); // Update with function
count.peek(); // Read without tracking
count.subscribe(fn); // Subscribe to changes
count.toStream(); // Convert to Stream
count.dispose(); // CleanupStreams
| Function | Description |
|----------|-------------|
| stream(options?) | Create a reactive stream |
| pipe(source, ...ops) | Compose operators |
| merge(...streams) | Merge multiple streams |
| combineLatest(...) | Combine latest values |
Stream Methods
const s = stream<number>();
s.next(1); // Emit value
s.error(err); // Emit error
s.complete(); // Complete stream
s.subscribe({...}); // Subscribe
s.pipe(op); // Apply operator
s.toSignal(0); // Convert to Signal
s.dispose(); // CleanupOperators (30+)
import {
// Transformation
map, mapTo, scan, reduce, buffer, pluck,
// Filtering
filter, take, skip, takeWhile, skipWhile,
distinct, distinctUntilChanged,
// Timing
debounce, throttle, delay, timeout, sample,
// Combination
merge, combineLatest, withLatestFrom, zip, race,
// Higher-Order
switchMap, mergeMap, concatMap, exhaustMap,
// Error Handling
catchError, retry, retryWhen,
// Utility
tap, finalize, defaultIfEmpty, count, find, isEmpty
} from '@oxog/pulse/operators';🎯 Usage Examples
Reactive Counter
import { signal, computed, effect } from '@oxog/pulse';
const count = signal(0);
const doubled = computed(() => count.get() * 2);
const quadrupled = computed(() => doubled.get() * 2);
effect(() => {
document.getElementById('count').textContent = count.get();
document.getElementById('doubled').textContent = doubled.get();
document.getElementById('quadrupled').textContent = quadrupled.get();
});
document.getElementById('increment').addEventListener('click', () => {
count.update(n => n + 1);
});Batch Updates
import { signal, computed, effect, batch } from '@oxog/pulse';
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName.get()} ${lastName.get()}`);
effect(() => console.log(fullName.get()));
// Logs: "John Doe"
// Without batch: effect runs twice
firstName.set('Jane');
lastName.set('Smith');
// Logs: "Jane Doe", then "Jane Smith"
// With batch: effect runs once
batch(() => {
firstName.set('Bob');
lastName.set('Johnson');
});
// Logs: "Bob Johnson" (only once!)Search with Debounce
import { stream, pipe } from '@oxog/pulse';
import { debounce, filter, switchMap } from '@oxog/pulse/operators';
const search$ = stream<string>();
const results$ = pipe(
search$,
filter(q => q.length >= 2),
debounce(300),
switchMap(async (q) => {
const res = await fetch(`/api/search?q=${q}`);
return res.json();
})
);
results$.subscribe(results => {
renderSearchResults(results);
});
// In your input handler
input.addEventListener('input', (e) => {
search$.next(e.target.value);
});React Integration
import { signal, computed } from '@oxog/pulse';
import { useSignal, useComputed } from '@oxog/pulse/react';
const count = signal(0);
const doubled = computed(() => count.get() * 2);
function Counter() {
const value = useSignal(count);
const dbl = useComputed(doubled);
return (
<div>
<p>Count: {value}</p>
<p>Doubled: {dbl}</p>
<button onClick={() => count.update(n => n + 1)}>
Increment
</button>
</div>
);
}🔌 Framework Integrations
| Framework | Import | Description |
|-----------|--------|-------------|
| React | @oxog/pulse/react | useSignal, useComputed, useStream |
| Vue | @oxog/pulse/vue | toVueRef, useSignal, toVueComputed |
| Svelte | @oxog/pulse/svelte | toSvelteStore, fromSvelteStore |
🏗️ Plugin Architecture
Built on @oxog/plugin micro-kernel:
import { getPulse } from '@oxog/pulse';
const pulse = getPulse();
// Install plugins
pulse.use(myCustomPlugin);
// Listen to events
pulse.on('signal:create', (data) => {
console.log('Signal created:', data);
});📊 Comparison
| Feature | @oxog/pulse | Preact Signals | RxJS | MobX | |---------|-------------|----------------|------|------| | TC39 Compliant | ✅ | ✅ | ❌ | ❌ | | Streams Built-in | ✅ | ❌ | ✅ | ❌ | | Zero Deps | ✅ | ✅ | ❌ | ❌ | | Bundle Size | ~15KB | ~2KB | ~60KB | ~16KB | | Framework Agnostic | ✅ | ❌ | ✅ | ✅ | | Plugin System | ✅ | ❌ | ❌ | ❌ |
📚 Documentation
🤝 Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
# Clone and setup
git clone https://github.com/ersinkoc/pulse.git
cd pulse
pnpm install
# Run tests
pnpm test
# Build
pnpm build📄 License
MIT © Ersin Koç
