Added comprehensive documentation for SigPro library, covering core philosophy, API reference, component system, state management, and advanced patterns.
1072 lines
27 KiB
Plaintext
1072 lines
27 KiB
Plaintext
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`<div>Hello ${name}</div>`
|
||
|
||
// Reactive function
|
||
html`<div>Count: ${() => count()}</div>`
|
||
|
||
// Multiple values
|
||
html`<div>${() => a()} + ${() => b()} = ${() => a() + b()}</div>`
|
||
|
||
// Conditional rendering (falsy values: null/undefined/false render nothing)
|
||
html`<div>${() => isLoading() ? spinner : content}</div>`
|
||
|
||
// List rendering (array of nodes or strings)
|
||
html`<ul>${() => items.map(item => html`<li>${item}</li>`)}</ul>`
|
||
|
||
// Mixed content
|
||
html`<div>${() => [header, content, footer]}</div>`
|
||
|
||
// Fragment handling - auto-wraps multiple root nodes
|
||
html`
|
||
<header>Title</header>
|
||
<main>Content</main>
|
||
<footer>Footer</footer>
|
||
` // Returns DocumentFragment with 3 children
|
||
```
|
||
|
||
**Event Directives - Complete Reference:**
|
||
```javascript
|
||
// Basic
|
||
html`<button @click=${handler}>Click</button>`
|
||
|
||
// Event object access
|
||
html`<input @input=${(e) => 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`<a @click.prevent.stop=${handler} href="#">No navigation, no bubble</a>`
|
||
html`<input @input.debounce:500=${search} placeholder="Search">`
|
||
html`<button @click.once=${oneTime}>Only once</button>`
|
||
html`<div @scroll.passive=${onScroll}>Optimized scroll</div>`
|
||
html`<div @click.capture=${captureHandler}>Capture phase</div>`
|
||
html`<button @click.self=${selfHandler}>Only if button clicked, not children</button>`
|
||
|
||
// Multiple modifiers
|
||
html`<input @keydown.debounce:300.prevent.stop=${handler}>`
|
||
|
||
// Dynamic event handlers
|
||
html`<button @click=${() => show() ? handler1 : handler2}>`
|
||
```
|
||
|
||
**Two-way Binding: `:property`**
|
||
```javascript
|
||
// Text input
|
||
html`<input :value=${userName}>`
|
||
|
||
// Checkbox
|
||
html`<input type="checkbox" :checked=${isAdmin}>`
|
||
|
||
// Radio group
|
||
html`
|
||
<input type="radio" name="size" value="small" :checked=${size === 'small'}>
|
||
<input type="radio" name="size" value="large" :checked=${size === 'large'}>
|
||
`
|
||
|
||
// Select
|
||
html`
|
||
<select :value=${selectedOption}>
|
||
<option value="a">A</option>
|
||
<option value="b">B</option>
|
||
</select>
|
||
`
|
||
|
||
// Textarea
|
||
html`<textarea :value=${content}></textarea>`
|
||
|
||
// Custom elements
|
||
html`<my-input :value=${data}></my-input>`
|
||
|
||
// Updates happen on 'input' for text, 'change' for checkboxes/radios
|
||
// Two-way binding creates two effects automatically
|
||
```
|
||
|
||
**Boolean Attributes: `?attribute`**
|
||
```javascript
|
||
html`<button ?disabled=${isDisabled}>`
|
||
html`<input ?required=${isRequired}>`
|
||
html`<details ?open=${expanded}>`
|
||
html`<option ?selected=${isSelected}>`
|
||
html`<div ?hidden=${!visible}>`
|
||
// Attribute present if truthy, absent if falsy
|
||
```
|
||
|
||
**Property Binding: `.property`**
|
||
```javascript
|
||
html`<div .scrollTop=${scrollPosition}>`
|
||
html`<progress .value=${progress} .max=${100}>`
|
||
html`<video .currentTime=${timestamp} .volume=${volume}>`
|
||
html`<my-element .data=${complexObject}>` // Pass objects directly
|
||
html`<div .classList=${['active', 'visible']}>` // Set classList property
|
||
|
||
// Property binding sets both property AND attribute (unless object/boolean)
|
||
```
|
||
|
||
**Special Cases:**
|
||
```javascript
|
||
// SVG support
|
||
html`<svg width="100" height="100">
|
||
<circle cx="50" cy="50" r=${() => radius()} fill="red"/>
|
||
</svg>`
|
||
|
||
// Self-closing tags
|
||
html`<img src=${imageUrl} alt=${altText}>`
|
||
|
||
// Comments - preserved in template
|
||
html`<!-- Header section --><h1>Title</h1>`
|
||
|
||
// Doctype - only at root
|
||
html`<!DOCTYPE html><html>...</html>`
|
||
|
||
// Template tag
|
||
html`<template>${() => repeatContent()}</template>`
|
||
|
||
// Slot handling
|
||
html`<my-component>${() => slottedContent}</my-component>`
|
||
```
|
||
|
||
### Component System: `$.component(tagName, setupFn, observedAttributes)`
|
||
Full Web Components integration with reactive properties.
|
||
|
||
**Complete Component Lifecycle:**
|
||
```javascript
|
||
$.component('my-counter', (props, ctx) => {
|
||
// 1. Initialization phase
|
||
console.log('Component initializing');
|
||
|
||
// Local state (not exposed as attribute)
|
||
const internalCount = $(0);
|
||
|
||
// Computed from props
|
||
const doubled = $(() => props.count() * 2);
|
||
|
||
// 2. Setup side effects
|
||
const stopEffect = $.effect(() => {
|
||
console.log('Count changed:', props.count());
|
||
});
|
||
|
||
// 3. Register cleanup
|
||
ctx.onUnmount(() => {
|
||
console.log('Cleaning up');
|
||
stopEffect();
|
||
});
|
||
|
||
// 4. Access DOM
|
||
ctx.select('button')?.classList.add('primary');
|
||
|
||
// 5. Handle slots
|
||
const defaultSlot = ctx.slot(); // unnamed slots
|
||
const headerSlot = ctx.slot('header'); // named slots
|
||
|
||
// 6. Emit events
|
||
const handleClick = () => {
|
||
ctx.emit('increment', { value: props.count() });
|
||
ctx.emit('counter-changed', { count: props.count() }, { bubbles: false }); // Custom options
|
||
};
|
||
|
||
// 7. Return template (must return Node or DocumentFragment)
|
||
return html`
|
||
<div class="counter">
|
||
<slot name="header">Default header</slot>
|
||
<button @click=${handleClick}>
|
||
Count: ${() => props.count()}
|
||
</button>
|
||
<slot>Default content</slot>
|
||
</div>
|
||
`;
|
||
}, ['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 = '<my-counter count="5"></my-counter>';
|
||
|
||
// 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`<slot></slot>`;
|
||
}, ['theme']);
|
||
|
||
$.component('themed-button', (props, ctx) => {
|
||
const theme = $('light');
|
||
|
||
// Consume from parent
|
||
ctx.host.addEventListener('theme-provider', (e) => {
|
||
theme(e.detail.theme);
|
||
});
|
||
|
||
return html`<button class=${() => theme()}>Click</button>`;
|
||
});
|
||
|
||
// Compound components
|
||
$.component('tabs', (props, ctx) => {
|
||
const activeTab = $(0);
|
||
|
||
return html`
|
||
<div class="tabs">
|
||
<div class="tab-headers">
|
||
${() => props.tabs().map((tab, i) => html`
|
||
<button @click=${() => activeTab(i)}>${tab.title}</button>
|
||
`)}
|
||
</div>
|
||
<div class="tab-content">
|
||
${() => props.tabs()[activeTab()].content}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}, ['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`<input :value=${props.value}>`;
|
||
}, ['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`<home-page></home-page>` },
|
||
{ path: '/about', component: () => html`<about-page></about-page>` },
|
||
|
||
// Parameterized routes
|
||
{
|
||
path: '/users/:id',
|
||
component: (params) => html`<user-profile user-id=${params.id}></user-profile>`
|
||
},
|
||
{
|
||
path: '/posts/:postId/comments/:commentId',
|
||
component: (params) => html`
|
||
<div>
|
||
Post ${params.postId}, Comment ${params.commentId}
|
||
</div>
|
||
`
|
||
},
|
||
|
||
// Regex routes with capture groups
|
||
{
|
||
path: /^\/products\/(?<category>\w+)\/(?<id>\d+)$/,
|
||
component: (params) => html`
|
||
<product-view
|
||
category=${params.category}
|
||
id=${params.id}
|
||
></product-view>
|
||
`
|
||
},
|
||
{
|
||
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`<search-results query=${searchParams.get('q')}></search-results>`;
|
||
}
|
||
},
|
||
|
||
// 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`<!-- Empty, redirecting -->`;
|
||
}
|
||
return html`<admin-panel></admin-panel>`;
|
||
}
|
||
},
|
||
|
||
// Catch-all / 404
|
||
{
|
||
path: /.*/,
|
||
component: () => html`<not-found></not-found>`
|
||
}
|
||
]);
|
||
|
||
// 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`<error-display error=${error()}></error-display>`
|
||
: html`<slot></slot>`;
|
||
});
|
||
|
||
// 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> = {
|
||
(): T;
|
||
(value: T | ((prev: T) => T)): void;
|
||
};
|
||
|
||
type Computed<T> = () => T;
|
||
|
||
type Effect = (fn: () => (void | (() => void))) => () => void;
|
||
|
||
// Component props typing
|
||
interface ComponentProps {
|
||
[key: string]: Signal<any>;
|
||
}
|
||
|
||
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<Effect>, // Pending effects
|
||
signal: {
|
||
subscribers: Set<Effect>, // Effects depending on this signal
|
||
value: any, // Current value
|
||
isDirty?: boolean // For computed signals
|
||
},
|
||
effect: {
|
||
dependencies: Set<Set<Effect>>, // Signals this effect depends on
|
||
cleanupHandlers: Set<Function>, // 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`
|
||
<div class="app">
|
||
<header>
|
||
<h1>Todo App</h1>
|
||
<todo-stats stats=${() => stats()}></todo-stats>
|
||
</header>
|
||
|
||
<todo-input @add=${(e) => addTodo(e.detail)}></todo-input>
|
||
|
||
<div class="filters">
|
||
<button @click=${() => filter('all')} ?active=${() => filter() === 'all'}>
|
||
All (${() => stats().total})
|
||
</button>
|
||
<button @click=${() => filter('active')} ?active=${() => filter() === 'active'}>
|
||
Active (${() => stats().active})
|
||
</button>
|
||
<button @click=${() => filter('completed')} ?active=${() => filter() === 'completed'}>
|
||
Completed (${() => stats().completed})
|
||
</button>
|
||
</div>
|
||
|
||
<todo-list
|
||
todos=${() => filteredTodos()}
|
||
@toggle=${(e) => toggleTodo(e.detail)}
|
||
@remove=${(e) => removeTodo(e.detail)}
|
||
></todo-list>
|
||
|
||
${() => ws.status() !== 'connected' ? html`
|
||
<div class="offline-banner">
|
||
Offline - ${() => ws.status()}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`);
|
||
|
||
$.component('todo-input', (props, ctx) => {
|
||
const text = $('');
|
||
|
||
const handleSubmit = () => {
|
||
if (text().trim()) {
|
||
ctx.emit('add', text().trim());
|
||
text('');
|
||
}
|
||
};
|
||
|
||
return html`
|
||
<div class="todo-input">
|
||
<input
|
||
:value=${text}
|
||
@keydown.enter=${handleSubmit}
|
||
placeholder="What needs to be done?"
|
||
>
|
||
<button @click=${handleSubmit}>Add</button>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
$.component('todo-list', (props) => html`
|
||
<ul class="todo-list">
|
||
${() => props.todos().map(todo => html`
|
||
<li class=${() => todo.completed ? 'completed' : ''}>
|
||
<input
|
||
type="checkbox"
|
||
:checked=${() => todo.completed}
|
||
@change=${() => props.emit('toggle', todo.id)}
|
||
>
|
||
<span>${todo.text}</span>
|
||
<button @click=${() => props.emit('remove', todo.id)}>×</button>
|
||
</li>
|
||
`)}
|
||
</ul>
|
||
`, ['todos']);
|
||
|
||
$.component('todo-stats', (props) => html`
|
||
<div class="stats">
|
||
<span>Total: ${() => props.stats().total}</span>
|
||
<span>Active: ${() => props.stats().active}</span>
|
||
<span>Completed: ${() => props.stats().completed}</span>
|
||
</div>
|
||
`, ['stats']);
|
||
|
||
// Router setup
|
||
const router = $.router([
|
||
{ path: '/', component: () => html`<todo-app></todo-app>` },
|
||
{ path: '/settings', component: () => html`<settings-page></settings-page>` },
|
||
{ path: '/about', component: () => html`<about-page></about-page>` }
|
||
]);
|
||
|
||
// 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.
|