The Problems Cereb Solves
Note: These are the problems Cereb solves.
1. No Abstraction for Event Flow
DOM events are just callbacks—there’s no structure for managing state, expressing dependencies between events, or composing multiple inputs. Consider a multi-input zoom implementation:
// Before: Scattered handlers, shared state, duplicated logiclet currentScale = 1;let isZoomMode = false;let initialPinchDistance = 0;
window.addEventListener('keydown', e => { if (e.key === 'z') { isZoomMode = true; } if (isZoomMode && (e.key === '+' || e.key === '-')) { e.preventDefault(); currentScale = Math.max(MIN, Math.min(MAX, currentScale * ...)); render(currentScale); }});window.addEventListener('keyup', e => { /* isZoomMode = false ... */ });
box.addEventListener('wheel', e => { if (!isZoomMode) return; currentScale = Math.max(MIN, Math.min(MAX, ...)); // duplicated render(currentScale);}, { passive: false });
// Pinch: touchstart/touchmove/touchend with distance calculation...box.addEventListener('touchstart', e => { /* ... */ });box.addEventListener('touchmove', e => { /* distance, ratio, min/max again */ });box.addEventListener('touchend', () => { /* cleanup */ });
// 8+ handlers, 3+ shared states, min/max duplicated everywhereCereb models events as streams, creating readable and composable pipelines:
// After: Stream abstraction—composable, stateless, explicitimport { keydown, keyheld, wheel, pinch } from "cereb";import { zoom, when, extend, spy } from "cereb/operators";
const zoomMode$ = keyheld(window, { code: "KeyZ" }) .pipe(extend((signal) => ({ opened: signal.value.held })));
const zoomOp = () => zoom({ minScale: 0.5, maxScale: 3.0, baseScale: getScale });
// Pinch zoompinch(element) .pipe(zoomOp()) .on(applyScale);
// z + wheel zoomwheel(element, { passive: false }) .pipe( when(zoomMode$), spy((signal) => signal.value.originalEvent.preventDefault()), extend((signal) => ({ ratio: Math.exp(-signal.value.deltaY * 0.005) })), zoomOp(), ) .on(applyScale);
// z + '+/-' zoomkeydown(window, { code: ["Equal", "Minus"] }) .pipe( when(zoomMode$), extend((signal) => ({ ratio: signal.value.code === "Equal" ? 1.2 : 1 / 1.2 })), zoomOp(), ) .on(applyScale);2. Lightweight Bundle Size
Benchmark: Equivalent pan gesture implementation
| Minified | Gzipped | |
|---|---|---|
| cereb (pan) | 4.58 KB | 1.73 KB |
| Hammer.js | 20.98 KB | 7.52 KB |
~77% smaller than Hammer.js for equivalent pan gesture functionality.
3. Performance & Resource Efficiency
Event Listener Reuse
// Before: Multiple addEventListener callswindow.addEventListener('keydown', handler1);window.addEventListener('keydown', handler2);window.addEventListener('keydown', handler3);
// After: Single native listener, multiple observersconst key$ = keyboard(window).pipe(share());key$.on(handler1);key$.on(handler2);key$.on(handler3);Single Responsibility Operators
pan(element) .pipe( offset({ target }), // Element-relative coordinates axisLock() // Lock to horizontal/vertical )Each operator does one thing well, and you compose them as needed.