No description
Find a file
2026-02-16 22:52:00 +02:00
docs init 2026-02-16 19:38:36 +02:00
examples init 2026-02-16 19:38:36 +02:00
src init 2026-02-16 19:38:36 +02:00
tests init 2026-02-16 19:38:36 +02:00
.gitignore init 2026-02-16 19:38:36 +02:00
CHANGELOG.md init 2026-02-16 19:38:36 +02:00
eslint.config.js init 2026-02-16 19:38:36 +02:00
LICENSE init 2026-02-16 19:38:36 +02:00
package-lock.json init 2026-02-16 19:38:36 +02:00
package.json Add publishConfig for public npm access 2026-02-16 22:52:00 +02:00
README.md init 2026-02-16 19:38:36 +02:00
tsconfig.json init 2026-02-16 19:38:36 +02:00
tsup.config.ts init 2026-02-16 19:38:36 +02:00
vitest.config.ts init 2026-02-16 19:38:36 +02:00

@oxog/pulse

A modern, high-performance reactive state management library with TC39 Signals compliance

npm version license bundle size types

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();    // Cleanup

Streams

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();         // Cleanup

Operators (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ç