diff --git a/llms.txt b/llms.txt deleted file mode 100644 index 0cfd1d7..0000000 --- a/llms.txt +++ /dev/null @@ -1,1067 +0,0 @@ -```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); -```