```txt # SigPro Complete System Documentation ## Library Identity SigPro is a zero-dependency reactive signal library (~5KB gzipped) for building reactive web applications with fine-grained reactivity, built-in DOM bindings, Web Components, routing, persistent storage, and WebSocket integration. ## Core Philosophy - Signals are functions, not objects with .value - Automatic dependency tracking (no manual declaration) - Computed values cache until dependencies change - DOM updates batched via microtask queue - Everything reactive composes naturally ## Complete API Reference ### Core Signal: `$(initialValue)` The fundamental reactive primitive. **Overload 1: Regular Signal** ```javascript const count = $(0); // Initial value count() // Get -> 0 count(5) // Set -> 5 count(prev => prev + 1) // Update with function count(prev => ({...prev, new: true})) // Object update ``` **Overload 2: Computed Signal** ```javascript const doubled = $(() => count() * 2); // Auto-detects count dependency const fullName = $(() => `${firstName()} ${lastName()}`); ``` **Advanced Patterns:** ```javascript // Lazy computed - only recalculates when accessed AND dependencies changed const expensive = $(() => { console.log('Computing...'); return data().filter(complexOperation); }); // Computed with cleanup (rare but possible) const withCleanup = $(() => { const temp = setup(); return () => cleanup(temp); // Runs when dependencies change }); // Circular dependency protection - throws error const a = $(() => b()); const b = $(() => a()); // Error: Circular dependency detected ``` ### Effect: `$.effect(fn)` Automatic side effect runner with intelligent cleanup. ```javascript // Basic tracking $.effect(() => { console.log(`Count is: ${count()}`); // Re-runs when count changes }); // Cleanup pattern $.effect(() => { const timer = setInterval(() => { console.log('tick', count()); }, 1000); // Return cleanup - runs BEFORE next effect run OR on stop return () => { clearInterval(timer); console.log('Cleaned up timer'); }; }); // Nested effects - parent tracks child cleanup $.effect(() => { const stopChild = $.effect(() => { // This child effect is automatically cleaned up // when parent re-runs or stops }); }); // Effect batching - multiple updates trigger one run count(1); count(2); count(3); // Effect runs once with final value 3 // Manual stop const stop = $.effect(() => {}); stop(); // Permanently stops effect, runs cleanup // Effect within effect - parent tracks child $.effect(() => { const data = fetchData(); $.effect(() => { // This child effect will be auto-cleaned // when parent re-runs process(data()); }); }); // Async effects - careful! Only sync dependencies tracked $.effect(async () => { const id = userId(); // Tracked ✅ const data = await fetch(`/api/${id}`); // Async // DO NOT access signals here - they won't be tracked! // Instead, move them before await or use separate effect }); ``` ### DOM Template: `` html`...` `` Complete reactive templating system. **Text Interpolation:** ```javascript // Basic html`
Hello ${name}
` // Reactive function html`
Count: ${() => count()}
` // Multiple values html`
${() => a()} + ${() => b()} = ${() => a() + b()}
` // Conditional rendering (falsy values: null/undefined/false render nothing) html`
${() => isLoading() ? spinner : content}
` // List rendering (array of nodes or strings) html`` // Mixed content html`
${() => [header, content, footer]}
` // Fragment handling - auto-wraps multiple root nodes html`
Title
Content
` // Returns DocumentFragment with 3 children ``` **Event Directives - Complete Reference:** ```javascript // Basic html`` // Event object access html` value(e.target.value)}>` // All modifiers: // prevent - e.preventDefault() // stop - e.stopPropagation() // once - auto-remove after first fire // self - only if e.target === element // debounce:ms - debounce handler // passive - { passive: true } // capture - use capture phase // Examples html`No navigation, no bubble` html`` html`` html`
Optimized scroll
` html`
Capture phase
` html`` // Multiple modifiers html`` // Dynamic event handlers html` Default content `; }, ['count', 'min', 'max']); // Observed attributes // Usage examples: const el = document.createElement('my-counter'); el.count = 5; // Set property el.setAttribute('min', '0'); // Set attribute el.min = 10; // Property takes precedence console.log(el.count); // Get property -> 10 console.log(el.getAttribute('count')); // Get attribute -> "10" // Special attribute conversion: el.disabled = ''; // true (except for 'value') el.disabled = 'false'; // false el.value = ''; // "" (empty string preserved) el.checked = 'false'; // false // Event listening: el.addEventListener('increment', (e) => { console.log(e.detail); // { value: x } }); // Dynamic component creation: const container = document.getElementById('app'); container.innerHTML = ''; // Or programmatically: const counter = new (customElements.get('my-counter'))(); counter.count = 5; container.appendChild(counter); ``` **Advanced Component Patterns:** ```javascript // Context/provider pattern $.component('theme-provider', (props, ctx) => { const theme = $(props.theme()); // Provide to children via custom event ctx.emit('theme-provider', { theme }); return html``; }, ['theme']); $.component('themed-button', (props, ctx) => { const theme = $('light'); // Consume from parent ctx.host.addEventListener('theme-provider', (e) => { theme(e.detail.theme); }); return html``; }); // Compound components $.component('tabs', (props, ctx) => { const activeTab = $(0); return html`
${() => props.tabs().map((tab, i) => html` `)}
${() => props.tabs()[activeTab()].content}
`; }, ['tabs']); // Form association $.component('my-input', (props, ctx) => { // Integrate with parent forms const form = ctx.host.closest('form'); if (form) { form.addEventListener('reset', () => { props.value(props.defaultValue()); }); } return html``; }, ['value', 'defaultValue']); ``` ### Persistent Storage: `$.storage(key, initialValue, storage?)` Automatic persistence with change detection. ```javascript // Basic localStorage const settings = $.storage('app-settings', { theme: 'light' }); settings({ ...settings(), theme: 'dark' }); // Auto-saves // Session storage const tempData = $.storage('temp', null, sessionStorage); // Multiple stores const userPrefs = $.storage('user-prefs', {}); const appState = $.storage('app-state', {}); // Auto-cleanup const cache = $.storage('cache', {}); cache(null); // Removes from storage // Complex objects - automatic JSON serialization const complex = $.storage('complex', { date: new Date(), regex: /pattern/, nested: { array: [1,2,3] } }); // Storage events - auto sync across tabs window.addEventListener('storage', (e) => { if (e.key === 'app-settings') { // Signal auto-updates on next read } }); // Migration pattern const data = $.storage('v2-data', () => { // Migration from v1 const old = localStorage.getItem('v1-data'); return old ? migrate(JSON.parse(old)) : defaultValue; }); // Computed from storage const config = $(() => { const base = settings(); return { ...base, computed: derive(base) }; }); ``` ### Router: `$.router(routes)` and `$.router.go()` Hash-based routing with parameter extraction. ```javascript // Route definitions - comprehensive examples const router = $.router([ // Static routes { path: '/', component: () => html`` }, { path: '/about', component: () => html`` }, // Parameterized routes { path: '/users/:id', component: (params) => html`` }, { path: '/posts/:postId/comments/:commentId', component: (params) => html`
Post ${params.postId}, Comment ${params.commentId}
` }, // Regex routes with capture groups { path: /^\/products\/(?\w+)\/(?\d+)$/, component: (params) => html` ` }, { path: /\/archive\/(\d{4})\/(\d{2})/, component: (params) => html`Archive: ${params[0]}-${params[1]}` }, // Query parameter handling (manual) { path: '/search', component: () => { const searchParams = new URLSearchParams(window.location.search); return html``; } }, // Nested routers { path: '/dashboard', component: () => { return $.router([ { path: '/', component: dashboardHome }, { path: '/settings', component: dashboardSettings } ]); } }, // Guard pattern { path: '/admin', component: (params) => { if (!isAuthenticated()) { $.router.go('/login'); return html``; } return html``; } }, // Catch-all / 404 { path: /.*/, component: () => html`` } ]); // Navigation $.router.go('/users/123'); // Basic $.router.go('users/123'); // Auto-adds leading slash $.router.go('/search?q=test'); // Query strings preserved // Advanced navigation const navigateWithState = (path, state) => { history.replaceState(state, '', `#${path}`); window.dispatchEvent(new HashChangeEvent('hashchange')); }; // Route change detection window.addEventListener('hashchange', () => { console.log('Route changed to:', window.location.hash); }); // Current route signal (if needed) const currentRoute = $(() => window.location.hash.replace(/^#/, '') || '/'); // Route params as signals const routeParams = $(null); $.effect(() => { const match = currentRoute().match(/\/users\/(\d+)/); routeParams(match ? { id: match[1] } : null); }); // Lazy loading routes const router = $.router([ { path: '/heavy', component: async (params) => { const module = await import('./heavy-page.js'); return module.default(params); } } ]); ``` ### Fetch: `$.fetch(url, data, loading?)` Simplified data fetching with loading state. ```javascript // Basic usage const result = await $.fetch('/api/users', { id: 123 }); // With loading signal const isLoading = $(false); const data = await $.fetch('/api/search', { query: 'test' }, isLoading); console.log(isLoading()); // false after completion // Error handling (returns null on error) const result = await $.fetch('/api/fail', {}) || fallbackData; // Integration with signals const userData = $(null); const loading = $(false); async function loadUser(id) { userData(await $.fetch(`/api/users/${id}`, {}, loading)); } // Reactive fetch with effect $.effect(() => { const id = currentUserId(); loadUser(id); }); // Retry logic wrapper async function fetchWithRetry(url, data, retries = 3) { for (let i = 0; i < retries; i++) { const result = await $.fetch(url, data); if (result) return result; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); } return null; } // Request cancellation let abortController = null; $.effect(() => { if (abortController) abortController.abort(); abortController = new AbortController(); const id = userId(); fetch(`/api/user/${id}`, { signal: abortController.signal }) .then(r => r.json()) .then(data => userData(data)); }); ``` ### WebSocket: `$.ws(url, options)` Full-featured WebSocket client with reactive state. ```javascript // Basic connection const ws = $.ws('wss://api.example.com/ws'); // With options const ws = $.ws('wss://api.example.com/ws', { reconnect: true, maxReconnect: 10, reconnectInterval: 2000 // Base interval for exponential backoff }); // Reactive state $.effect(() => { console.log('Status:', ws.status()); // 'connecting', 'connected', 'disconnected', 'error' console.log('Messages:', ws.messages()); // Array of received messages console.log('Error:', ws.error()); // Last error or null }); // Sending messages ws.send({ type: 'join', room: 'general' }); ws.send('plain text message'); // Auto-reconnect with backoff // Attempts: 2s, 4s, 8s, 16s, 32s (until maxReconnect) // Message filtering const commands = $(() => ws.messages().filter(m => m.type === 'command')); const events = $(() => ws.messages().filter(m => m.type === 'event')); // Send with acknowledgment async function sendWithAck(data, timeout = 5000) { return new Promise((resolve, reject) => { const id = Math.random(); const message = { ...data, id }; const handler = (msg) => { if (msg.ack === id) { ws.messages.off(handler); resolve(msg); } }; // Need message event listener pattern const timeoutId = setTimeout(() => { ws.messages.off(handler); reject(new Error('Ack timeout')); }, timeout); // Custom listener would be needed for this pattern ws.send(message); }); } // Reconnection handling $.effect(() => { if (ws.status() === 'connected') { console.log('Connected, sending init...'); ws.send({ type: 'init', token: authToken() }); } }); // Binary data const ws = $.ws('wss://example.com/binary'); ws.send(new Blob([data])); ws.send(new ArrayBuffer(8)); // Heartbeat / ping-pong $.effect(() => { if (ws.status() !== 'connected') return; const interval = setInterval(() => { ws.send({ type: 'ping' }); }, 30000); return () => clearInterval(interval); }); // Queue messages while disconnected const messageQueue = $([]); $.effect(() => { if (ws.status() === 'connected') { messageQueue().forEach(msg => ws.send(msg)); messageQueue([]); } }); function sendOrQueue(data) { if (ws.status() === 'connected') { ws.send(data); } else { messageQueue([...messageQueue(), data]); } } ``` ### Advanced Patterns & Best Practices **State Management Patterns:** ```javascript // Store pattern const createStore = (initial) => { const state = $(initial); return { state, actions: { update: (fn) => state(fn(state())), reset: () => state(initial) } }; }; // Selector pattern const selectUser = (id) => $(() => users().find(u => u.id === id) ); // Computed selector const selectVisibleTodos = $(() => todos().filter(t => filter() === 'all' || (filter() === 'active' && !t.completed) || (filter() === 'completed' && t.completed) ) ); // Action pattern const increment = () => count(c => c + 1); const addTodo = (text) => todos(t => [...t, { text, completed: false }]); const toggleTodo = (id) => todos(t => t.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); ``` **Performance Optimization:** ```javascript // Memoization const expensiveComputation = $(() => { console.log('Computing...'); return bigData().filter(heavyFilter).map(transform); }); // Batch updates count(1); count(2); count(3); // Single effect run // Manual flush if needed import { flushEffectQueue } from 'sigpro'; flushEffectQueue(); // Force immediate effect processing // Lazy computed - only computes when accessed const lazyValue = $(() => { if (!cache) cache = expensive(); return cache; }); // Effect debouncing let timeout; $.effect(() => { clearTimeout(timeout); timeout = setTimeout(() => { console.log('Debounced:', value()); }, 100); }); ``` **Error Boundaries & Recovery:** ```javascript // Effect error handling $.effect(() => { try { riskyOperation(); } catch (e) { errorSignal(e); } }); // Component error boundary $.component('error-boundary', (props, ctx) => { const error = $(null); const handleError = (e) => { error(e.error); e.preventDefault(); }; ctx.host.addEventListener('error', handleError); ctx.onUnmount(() => ctx.host.removeEventListener('error', handleError)); return () => error() ? html`` : html``; }); // Global error handler window.addEventListener('error', (e) => { console.error('SigPro error:', e.error); }); ``` **Testing Utilities:** ```javascript // Test helper pattern const createTestHarness = () => { const effects = []; const origEffect = $.effect; $.effect = (fn) => { const stop = origEffect(fn); effects.push(stop); return stop; }; return { cleanup: () => effects.forEach(stop => stop()), restore: () => { $.effect = origEffect; } }; }; // Async test helper const waitForEffect = () => new Promise(r => setTimeout(r, 0)); // Usage in tests test('counter increments', async () => { const count = $(0); const calls = []; $.effect(() => { calls.push(count()); }); count(1); await waitForEffect(); // Wait for effect queue expect(calls).toEqual([0, 1]); }); ``` **TypeScript Integration (conceptual):** ```typescript // Type patterns (even though library is JS) type Signal = { (): T; (value: T | ((prev: T) => T)): void; }; type Computed = () => T; type Effect = (fn: () => (void | (() => void))) => () => void; // Component props typing interface ComponentProps { [key: string]: Signal; } interface ComponentContext { select: (selector: string) => Element | null; slot: (name?: string) => Node[]; emit: (name: string, detail?: any, options?: CustomEventInit) => void; host: HTMLElement; onUnmount: (fn: () => void) => void; } ``` **Migration from Other Frameworks:** ```javascript // From Vue: // ref(0) -> $(0) // computed -> $(() => value) // watch -> $.effect // onMounted -> $.effect (runs immediately) // From React: // useState -> $ (but get/set combined) // useEffect -> $.effect // useMemo -> $(() => value) // useCallback -> Just use function, dependencies automatic // From Svelte: // let count = 0 -> const count = $(0) // $: doubled = count * 2 -> const doubled = $(() => count()) ``` ## Internal Architecture (for deep understanding) ```javascript // Reactivity graph structure { activeEffect: Effect | null, // Currently executing effect effectQueue: Set, // Pending effects signal: { subscribers: Set, // Effects depending on this signal value: any, // Current value isDirty?: boolean // For computed signals }, effect: { dependencies: Set>, // Signals this effect depends on cleanupHandlers: Set, // Cleanup functions markDirty?: Function // For computed signal effects } } // Flush mechanism // 1. Signal update adds affected effects to queue // 2. Schedules microtask flush // 3. Effects run in order added // 4. Each effect cleans up old dependencies // 5. New dependencies tracked during run ``` ## Complete Example Application ```javascript // app.js import { $, html, $.component, $.router, $.storage, $.ws } from 'sigpro'; // State const todos = $.storage('todos', []); const filter = $('all'); const user = $(null); const ws = $.ws('wss://api.example.com/todos'); // Computed const filteredTodos = $(() => { const all = todos(); switch(filter()) { case 'active': return all.filter(t => !t.completed); case 'completed': return all.filter(t => t.completed); default: return all; } }); const stats = $(() => { const all = todos(); return { total: all.length, active: all.filter(t => !t.completed).length, completed: all.filter(t => t.completed).length }; }); // Actions const addTodo = (text) => { const newTodo = { id: Date.now(), text, completed: false, createdAt: new Date().toISOString() }; todos([...todos(), newTodo]); ws.send({ type: 'add', todo: newTodo }); }; const toggleTodo = (id) => { todos(todos().map(t => t.id === id ? { ...t, completed: !t.completed } : t )); }; const removeTodo = (id) => { todos(todos().filter(t => t.id !== id)); }; // Sync with WebSocket $.effect(() => { ws.messages().forEach(msg => { if (msg.type === 'sync') { todos(msg.todos); } }); }); // Components $.component('todo-app', () => html`

Todo App

stats()}>
addTodo(e.detail)}>
filteredTodos()} @toggle=${(e) => toggleTodo(e.detail)} @remove=${(e) => removeTodo(e.detail)} > ${() => ws.status() !== 'connected' ? html`
Offline - ${() => ws.status()}
` : ''}
`); $.component('todo-input', (props, ctx) => { const text = $(''); const handleSubmit = () => { if (text().trim()) { ctx.emit('add', text().trim()); text(''); } }; return html`
`; }); $.component('todo-list', (props) => html`
    ${() => props.todos().map(todo => html`
  • todo.completed ? 'completed' : ''}> todo.completed} @change=${() => props.emit('toggle', todo.id)} > ${todo.text}
  • `)}
`, ['todos']); $.component('todo-stats', (props) => html`
Total: ${() => props.stats().total} Active: ${() => props.stats().active} Completed: ${() => props.stats().completed}
`, ['stats']); // Router setup const router = $.router([ { path: '/', component: () => html`` }, { path: '/settings', component: () => html`` }, { path: '/about', component: () => html`` } ]); // Mount app document.body.appendChild(router); ```