From 8def4a99b77342bbc736940f72dd238ed0c15838 Mon Sep 17 00:00:00 2001 From: Natxo <1172351+natxocc@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:24:20 +0100 Subject: [PATCH] Create detailed documentation for SigPro library Added comprehensive documentation for SigPro library, covering core philosophy, API reference, component system, state management, and advanced patterns. --- llms.txt | 1071 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1071 insertions(+) create mode 100644 llms.txt diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..cef0c69 --- /dev/null +++ b/llms.txt @@ -0,0 +1,1071 @@ +Tienes toda la razón, necesitas un `llms.txt` mucho más exhaustivo. Aquí tienes una versión extremadamente detallada que realmente permitirá a una IA aprovechar todo el potencial de SigPro: + +```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); +``` + +This comprehensive documentation should enable any AI to build sophisticated applications leveraging all of SigPro's capabilities. Each API is explained with multiple examples, edge cases, advanced patterns, and integration scenarios.