sigpro 1.0.14 → 1.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/Readme.md +164 -1008
  2. package/dist/sigpro.editor.js +1 -0
  3. package/dist/sigpro.grid.js +78 -0
  4. package/dist/sigpro.js +1 -0
  5. package/dist/sigpro.ui.css +2 -0
  6. package/dist/sigpro.ui.js +1 -0
  7. package/dist/sigpro.utils.js +1 -0
  8. package/dist/sigpro.vite.js +4 -0
  9. package/package.json +64 -14
  10. package/sigpro.d.ts +395 -0
  11. package/.github/workflows/publish.yml +0 -25
  12. package/bun.lock +0 -385
  13. package/docs/404.html +0 -22
  14. package/docs/api/components.html +0 -595
  15. package/docs/api/effects.html +0 -787
  16. package/docs/api/fetch.html +0 -873
  17. package/docs/api/pages.html +0 -405
  18. package/docs/api/quick.html +0 -217
  19. package/docs/api/routing.html +0 -628
  20. package/docs/api/signals.html +0 -683
  21. package/docs/api/storage.html +0 -820
  22. package/docs/assets/api_components.md.BlFwj17l.js +0 -571
  23. package/docs/assets/api_components.md.BlFwj17l.lean.js +0 -1
  24. package/docs/assets/api_effects.md.Br_yStBS.js +0 -763
  25. package/docs/assets/api_effects.md.Br_yStBS.lean.js +0 -1
  26. package/docs/assets/api_fetch.md.DQLBJSoq.js +0 -849
  27. package/docs/assets/api_fetch.md.DQLBJSoq.lean.js +0 -1
  28. package/docs/assets/api_pages.md.BP19nHXw.js +0 -381
  29. package/docs/assets/api_pages.md.BP19nHXw.lean.js +0 -1
  30. package/docs/assets/api_quick.md.BDS3ttnt.js +0 -193
  31. package/docs/assets/api_quick.md.BDS3ttnt.lean.js +0 -1
  32. package/docs/assets/api_routing.md.7SNAZXtp.js +0 -604
  33. package/docs/assets/api_routing.md.7SNAZXtp.lean.js +0 -1
  34. package/docs/assets/api_signals.md.CrW68-BA.js +0 -659
  35. package/docs/assets/api_signals.md.CrW68-BA.lean.js +0 -1
  36. package/docs/assets/api_storage.md.COEWBXHk.js +0 -796
  37. package/docs/assets/api_storage.md.COEWBXHk.lean.js +0 -1
  38. package/docs/assets/app.DtmzNmNl.js +0 -1
  39. package/docs/assets/chunks/framework.C8AWLET_.js +0 -19
  40. package/docs/assets/chunks/theme.yfWKMLQM.js +0 -1
  41. package/docs/assets/guide_getting-started.md.BeQpK3vd.js +0 -172
  42. package/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js +0 -1
  43. package/docs/assets/guide_why.md.DXchYMN-.js +0 -23
  44. package/docs/assets/guide_why.md.DXchYMN-.lean.js +0 -1
  45. package/docs/assets/index.md.uvMJmU4o.js +0 -1
  46. package/docs/assets/index.md.uvMJmU4o.lean.js +0 -1
  47. package/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  48. package/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  49. package/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  50. package/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  51. package/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  52. package/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  53. package/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  54. package/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  55. package/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  56. package/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  57. package/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  58. package/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  59. package/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  60. package/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  61. package/docs/assets/style.DJRheFKp.css +0 -1
  62. package/docs/assets/ui_intro.md.gZ21GFqo.js +0 -1
  63. package/docs/assets/ui_intro.md.gZ21GFqo.lean.js +0 -1
  64. package/docs/assets/vite_plugin.md.gDWEi8f0.js +0 -225
  65. package/docs/assets/vite_plugin.md.gDWEi8f0.lean.js +0 -1
  66. package/docs/guide/getting-started.html +0 -196
  67. package/docs/guide/why.html +0 -47
  68. package/docs/hashmap.json +0 -1
  69. package/docs/index.html +0 -25
  70. package/docs/logo.svg +0 -118
  71. package/docs/ui/intro.html +0 -25
  72. package/docs/vite/plugin.html +0 -249
  73. package/docs/vp-icons.css +0 -1
  74. package/index.js +0 -3
  75. package/packages/docs/.vitepress/cache/deps/@theme_index.js +0 -275
  76. package/packages/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
  77. package/packages/docs/.vitepress/cache/deps/_metadata.json +0 -40
  78. package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js +0 -12951
  79. package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map +0 -7
  80. package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js +0 -9719
  81. package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map +0 -7
  82. package/packages/docs/.vitepress/cache/deps/package.json +0 -3
  83. package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
  84. package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  85. package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
  86. package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  87. package/packages/docs/.vitepress/cache/deps/vue.js +0 -347
  88. package/packages/docs/.vitepress/cache/deps/vue.js.map +0 -7
  89. package/packages/docs/.vitepress/config.js +0 -68
  90. package/packages/docs/api/components.md +0 -760
  91. package/packages/docs/api/effects.md +0 -1039
  92. package/packages/docs/api/fetch.md +0 -998
  93. package/packages/docs/api/pages.md +0 -497
  94. package/packages/docs/api/quick.md +0 -436
  95. package/packages/docs/api/routing.md +0 -784
  96. package/packages/docs/api/signals.md +0 -899
  97. package/packages/docs/api/storage.md +0 -952
  98. package/packages/docs/guide/getting-started.md +0 -308
  99. package/packages/docs/guide/why.md +0 -135
  100. package/packages/docs/index.md +0 -84
  101. package/packages/docs/logo.svg +0 -118
  102. package/packages/docs/public/logo.svg +0 -118
  103. package/packages/docs/ui/intro.md +0 -16
  104. package/packages/docs/vite/plugin.md +0 -423
  105. package/packages/sigpro/plugin.js +0 -91
  106. package/packages/sigpro/plugin.min.js +0 -1
  107. package/packages/sigpro/sigpro.js +0 -631
  108. package/packages/sigpro/sigpro.min.js +0 -1
  109. package/vite.config.js +0 -24
@@ -1,1039 +0,0 @@
1
- # Effects API 🔄
2
-
3
- Effects are the bridge between reactive signals and side effects in your application. They automatically track signal dependencies and re-run whenever those signals change, enabling everything from DOM updates to data fetching and localStorage synchronization.
4
-
5
- ## Core Concepts
6
-
7
- ### What is an Effect?
8
-
9
- An effect is a function that:
10
- - **Runs immediately** when created
11
- - **Tracks all signals** read during its execution
12
- - **Re-runs automatically** when any tracked signal changes
13
- - **Can return a cleanup function** that runs before the next execution or when the effect is stopped
14
-
15
- ### How Effects Work
16
-
17
- 1. When an effect runs, it sets itself as the `activeEffect`
18
- 2. Any signal read during execution adds the effect to its subscribers
19
- 3. When a signal changes, it queues all its subscribers
20
- 4. Effects are batched and run in the next microtask
21
- 5. If an effect returns a function, it's stored as a cleanup handler
22
-
23
- ## `$.effect(effectFn)`
24
-
25
- Creates a reactive effect that automatically tracks dependencies and re-runs when they change.
26
-
27
- ```javascript
28
- import { $ } from 'sigpro';
29
-
30
- const count = $(0);
31
-
32
- $.effect(() => {
33
- console.log(`Count is: ${count()}`);
34
- });
35
- // Logs: "Count is: 0"
36
-
37
- count(1);
38
- // Logs: "Count is: 1"
39
- ```
40
-
41
- ## 📋 API Reference
42
-
43
- | Pattern | Example | Description |
44
- |---------|---------|-------------|
45
- | Basic Effect | `$.effect(() => console.log(count()))` | Run on dependency changes |
46
- | With Cleanup | `$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })` | Return cleanup function |
47
- | Stop Effect | `const stop = $.effect(...); stop()` | Manually stop an effect |
48
-
49
- ### Effect Object (Internal)
50
-
51
- | Property/Method | Description |
52
- |-----------------|-------------|
53
- | `dependencies` | Set of signal subscriber sets this effect belongs to |
54
- | `cleanupHandlers` | Set of cleanup functions to run before next execution |
55
- | `run()` | Executes the effect and tracks dependencies |
56
- | `stop()` | Stops the effect and runs all cleanup handlers |
57
-
58
- ## 🎯 Basic Examples
59
-
60
- ### Console Logging
61
-
62
- ```javascript
63
- import { $ } from 'sigpro';
64
-
65
- const name = $('World');
66
- const count = $(0);
67
-
68
- $.effect(() => {
69
- console.log(`Hello ${name()}! Count is ${count()}`);
70
- });
71
- // Logs: "Hello World! Count is 0"
72
-
73
- name('John');
74
- // Logs: "Hello John! Count is 0"
75
-
76
- count(5);
77
- // Logs: "Hello John! Count is 5"
78
- ```
79
-
80
- ### DOM Updates
81
-
82
- ```javascript
83
- import { $ } from 'sigpro';
84
-
85
- const count = $(0);
86
- const element = document.getElementById('counter');
87
-
88
- $.effect(() => {
89
- element.textContent = `Count: ${count()}`;
90
- });
91
-
92
- // Updates DOM automatically when count changes
93
- count(10); // Element text becomes "Count: 10"
94
- ```
95
-
96
- ### Document Title
97
-
98
- ```javascript
99
- import { $ } from 'sigpro';
100
-
101
- const page = $('home');
102
- const unreadCount = $(0);
103
-
104
- $.effect(() => {
105
- const base = page() === 'home' ? 'Home' : 'Dashboard';
106
- const unread = unreadCount() > 0 ? ` (${unreadCount()})` : '';
107
- document.title = `${base}${unread} - My App`;
108
- });
109
-
110
- page('dashboard'); // Title: "Dashboard - My App"
111
- unreadCount(3); // Title: "Dashboard (3) - My App"
112
- ```
113
-
114
- ## 🧹 Effects with Cleanup
115
-
116
- Cleanup functions are essential for managing resources like intervals, event listeners, and subscriptions.
117
-
118
- ### Basic Cleanup
119
-
120
- ```javascript
121
- import { $ } from 'sigpro';
122
-
123
- const userId = $(1);
124
-
125
- $.effect(() => {
126
- const id = userId();
127
- console.log(`Setting up timer for user ${id}`);
128
-
129
- const timer = setInterval(() => {
130
- console.log(`Polling user ${id}...`);
131
- }, 1000);
132
-
133
- // Cleanup runs before next effect execution
134
- return () => {
135
- console.log(`Cleaning up timer for user ${id}`);
136
- clearInterval(timer);
137
- };
138
- });
139
- // Sets up timer for user 1
140
-
141
- userId(2);
142
- // Cleans up timer for user 1
143
- // Sets up timer for user 2
144
- ```
145
-
146
- ### Event Listener Cleanup
147
-
148
- ```javascript
149
- import { $ } from 'sigpro';
150
-
151
- const isListening = $(false);
152
-
153
- $.effect(() => {
154
- if (!isListening()) return;
155
-
156
- const handleClick = (e) => {
157
- console.log('Window clicked:', e.clientX, e.clientY);
158
- };
159
-
160
- window.addEventListener('click', handleClick);
161
- console.log('Click listener added');
162
-
163
- return () => {
164
- window.removeEventListener('click', handleClick);
165
- console.log('Click listener removed');
166
- };
167
- });
168
-
169
- isListening(true); // Adds listener
170
- isListening(false); // Removes listener
171
- isListening(true); // Adds listener again
172
- ```
173
-
174
- ### WebSocket Connection
175
-
176
- ```javascript
177
- import { $ } from 'sigpro';
178
-
179
- const room = $('general');
180
- const messages = $([]);
181
-
182
- $.effect(() => {
183
- const currentRoom = room();
184
- console.log(`Connecting to room: ${currentRoom}`);
185
-
186
- const ws = new WebSocket(`wss://chat.example.com/${currentRoom}`);
187
-
188
- ws.onmessage = (event) => {
189
- messages([...messages(), JSON.parse(event.data)]);
190
- };
191
-
192
- ws.onerror = (error) => {
193
- console.error('WebSocket error:', error);
194
- };
195
-
196
- // Cleanup: close connection when room changes
197
- return () => {
198
- console.log(`Disconnecting from room: ${currentRoom}`);
199
- ws.close();
200
- };
201
- });
202
-
203
- room('random'); // Closes 'general' connection, opens 'random'
204
- ```
205
-
206
- ## ⏱️ Effect Timing and Batching
207
-
208
- ### Microtask Batching
209
-
210
- Effects are batched using `queueMicrotask` for optimal performance:
211
-
212
- ```javascript
213
- import { $ } from 'sigpro';
214
-
215
- const a = $(1);
216
- const b = $(2);
217
- const c = $(3);
218
-
219
- $.effect(() => {
220
- console.log('Effect ran with:', a(), b(), c());
221
- });
222
- // Logs immediately: "Effect ran with: 1 2 3"
223
-
224
- // Multiple updates in same tick - only one effect run!
225
- a(10);
226
- b(20);
227
- c(30);
228
- // Only logs once: "Effect ran with: 10 20 30"
229
- ```
230
-
231
- ### Async Effects
232
-
233
- Effects can be asynchronous, but be careful with dependency tracking:
234
-
235
- ```javascript
236
- import { $ } from 'sigpro';
237
-
238
- const userId = $(1);
239
- const userData = $(null);
240
-
241
- $.effect(() => {
242
- const id = userId();
243
- console.log(`Fetching user ${id}...`);
244
-
245
- // Only id() is tracked (synchronous part)
246
- fetch(`/api/users/${id}`)
247
- .then(res => res.json())
248
- .then(data => {
249
- // This runs later - no dependency tracking here!
250
- userData(data);
251
- });
252
- });
253
-
254
- userId(2); // Triggers effect again, cancels previous fetch
255
- ```
256
-
257
- ### Effect with AbortController
258
-
259
- For proper async cleanup with fetch:
260
-
261
- ```javascript
262
- import { $ } from 'sigpro';
263
-
264
- const userId = $(1);
265
- const userData = $(null);
266
- const loading = $(false);
267
-
268
- $.effect(() => {
269
- const id = userId();
270
- const controller = new AbortController();
271
-
272
- loading(true);
273
-
274
- fetch(`/api/users/${id}`, { signal: controller.signal })
275
- .then(res => res.json())
276
- .then(data => {
277
- userData(data);
278
- loading(false);
279
- })
280
- .catch(err => {
281
- if (err.name !== 'AbortError') {
282
- console.error('Fetch error:', err);
283
- loading(false);
284
- }
285
- });
286
-
287
- // Cleanup: abort fetch if userId changes before completion
288
- return () => {
289
- controller.abort();
290
- };
291
- });
292
- ```
293
-
294
- ## 🎨 Advanced Effect Patterns
295
-
296
- ### Debounced Effects
297
-
298
- ```javascript
299
- import { $ } from 'sigpro';
300
-
301
- const searchTerm = $('');
302
- const results = $([]);
303
- let debounceTimeout;
304
-
305
- $.effect(() => {
306
- const term = searchTerm();
307
-
308
- // Clear previous timeout
309
- clearTimeout(debounceTimeout);
310
-
311
- // Don't search if term is too short
312
- if (term.length < 3) {
313
- results([]);
314
- return;
315
- }
316
-
317
- // Debounce search
318
- debounceTimeout = setTimeout(async () => {
319
- console.log('Searching for:', term);
320
- const data = await fetch(`/api/search?q=${term}`).then(r => r.json());
321
- results(data);
322
- }, 300);
323
-
324
- // Cleanup on effect re-run
325
- return () => clearTimeout(debounceTimeout);
326
- });
327
- ```
328
-
329
- ### Throttled Effects
330
-
331
- ```javascript
332
- import { $ } from 'sigpro';
333
-
334
- const scrollPosition = $(0);
335
- let lastRun = 0;
336
- let rafId = null;
337
-
338
- $.effect(() => {
339
- const pos = scrollPosition();
340
-
341
- // Throttle with requestAnimationFrame
342
- if (rafId) cancelAnimationFrame(rafId);
343
-
344
- rafId = requestAnimationFrame(() => {
345
- console.log('Scroll position:', pos);
346
- updateScrollUI(pos);
347
- lastRun = Date.now();
348
- rafId = null;
349
- });
350
-
351
- return () => {
352
- if (rafId) {
353
- cancelAnimationFrame(rafId);
354
- rafId = null;
355
- }
356
- };
357
- });
358
-
359
- // Even with many updates, effect runs at most once per frame
360
- for (let i = 0; i < 100; i++) {
361
- scrollPosition(i);
362
- }
363
- ```
364
-
365
- ### Conditional Effects
366
-
367
- ```javascript
368
- import { $ } from 'sigpro';
369
-
370
- const isEnabled = $(false);
371
- const value = $(0);
372
- const threshold = $(10);
373
-
374
- $.effect(() => {
375
- // Effect only runs when isEnabled is true
376
- if (!isEnabled()) return;
377
-
378
- console.log(`Monitoring value: ${value()}, threshold: ${threshold()}`);
379
-
380
- if (value() > threshold()) {
381
- alert(`Value ${value()} exceeded threshold ${threshold()}!`);
382
- }
383
- });
384
-
385
- isEnabled(true); // Effect starts monitoring
386
- value(15); // Triggers alert
387
- isEnabled(false); // Effect stops (still runs, but condition prevents logic)
388
- ```
389
-
390
- ### Effect with Multiple Cleanups
391
-
392
- ```javascript
393
- import { $ } from 'sigpro';
394
-
395
- const config = $({ theme: 'light', notifications: true });
396
-
397
- $.effect(() => {
398
- const { theme, notifications } = config();
399
- const cleanups = [];
400
-
401
- // Setup theme
402
- document.body.className = `theme-${theme}`;
403
- cleanups.push(() => {
404
- document.body.classList.remove(`theme-${theme}`);
405
- });
406
-
407
- // Setup notifications
408
- if (notifications) {
409
- const handler = (e) => console.log('Notification:', e.detail);
410
- window.addEventListener('notification', handler);
411
- cleanups.push(() => {
412
- window.removeEventListener('notification', handler);
413
- });
414
- }
415
-
416
- // Return combined cleanup
417
- return () => {
418
- cleanups.forEach(cleanup => cleanup());
419
- };
420
- });
421
- ```
422
-
423
- ## 🎯 Effects in Components
424
-
425
- ### Component Lifecycle
426
-
427
- ```javascript
428
- import { $, html } from 'sigpro';
429
-
430
- $.component('timer-display', () => {
431
- const seconds = $(0);
432
-
433
- // Effect for timer - automatically cleaned up when component unmounts
434
- $.effect(() => {
435
- const interval = setInterval(() => {
436
- seconds(s => s + 1);
437
- }, 1000);
438
-
439
- return () => clearInterval(interval);
440
- });
441
-
442
- return html`
443
- <div>
444
- <h2>Timer: ${seconds}s</h2>
445
- </div>
446
- `;
447
- });
448
- ```
449
-
450
- ### Effects with Props
451
-
452
- ```javascript
453
- import { $, html } from 'sigpro';
454
-
455
- $.component('data-viewer', (props) => {
456
- const data = $(null);
457
- const error = $(null);
458
-
459
- // Effect reacts to prop changes
460
- $.effect(() => {
461
- const url = props.url();
462
- if (!url) return;
463
-
464
- const controller = new AbortController();
465
-
466
- fetch(url, { signal: controller.signal })
467
- .then(res => res.json())
468
- .then(data)
469
- .catch(err => {
470
- if (err.name !== 'AbortError') {
471
- error(err.message);
472
- }
473
- });
474
-
475
- return () => controller.abort();
476
- });
477
-
478
- return html`
479
- <div>
480
- ${() => {
481
- if (error()) return html`<div class="error">${error()}</div>`;
482
- if (!data()) return html`<div>Loading...</div>`;
483
- return html`<pre>${JSON.stringify(data(), null, 2)}</pre>`;
484
- }}
485
- </div>
486
- `;
487
- }, ['url']);
488
- ```
489
-
490
- ## 🔧 Effect Management
491
-
492
- ### Stopping Effects
493
-
494
- ```javascript
495
- import { $ } from 'sigpro';
496
-
497
- const count = $(0);
498
-
499
- // Start effect
500
- const stopEffect = $.effect(() => {
501
- console.log('Count:', count());
502
- });
503
-
504
- count(1); // Logs: "Count: 1"
505
- count(2); // Logs: "Count: 2"
506
-
507
- // Stop the effect
508
- stopEffect();
509
-
510
- count(3); // No logging - effect is stopped
511
- ```
512
-
513
- ### Conditional Effect Stopping
514
-
515
- ```javascript
516
- import { $ } from 'sigpro';
517
-
518
- const isActive = $(true);
519
- const count = $(0);
520
-
521
- let currentEffect = null;
522
-
523
- $.effect(() => {
524
- if (isActive()) {
525
- // Start or restart the monitoring effect
526
- if (currentEffect) currentEffect();
527
-
528
- currentEffect = $.effect(() => {
529
- console.log('Monitoring count:', count());
530
- });
531
- } else {
532
- // Stop monitoring
533
- if (currentEffect) {
534
- currentEffect();
535
- currentEffect = null;
536
- }
537
- }
538
- });
539
- ```
540
-
541
- ### Nested Effects
542
-
543
- ```javascript
544
- import { $ } from 'sigpro';
545
-
546
- const user = $({ id: 1, name: 'John' });
547
- const settings = $({ theme: 'dark' });
548
-
549
- $.effect(() => {
550
- console.log('User changed:', user().name);
551
-
552
- // Nested effect - tracks settings independently
553
- $.effect(() => {
554
- console.log('Settings changed:', settings().theme);
555
- });
556
-
557
- // When user changes, the nested effect is recreated
558
- });
559
- ```
560
-
561
- ## 🚀 Real-World Examples
562
-
563
- ### Auto-saving Form
564
-
565
- ```javascript
566
- import { $ } from 'sigpro';
567
-
568
- const formData = $({
569
- title: '',
570
- content: '',
571
- tags: []
572
- });
573
-
574
- const lastSaved = $(null);
575
- const saveStatus = $('idle'); // 'idle', 'saving', 'saved', 'error'
576
- let saveTimeout;
577
-
578
- $.effect(() => {
579
- const data = formData();
580
-
581
- // Clear previous timeout
582
- clearTimeout(saveTimeout);
583
-
584
- // Don't save empty form
585
- if (!data.title && !data.content) {
586
- saveStatus('idle');
587
- return;
588
- }
589
-
590
- saveStatus('saving');
591
-
592
- // Debounce save
593
- saveTimeout = setTimeout(async () => {
594
- try {
595
- await fetch('/api/posts', {
596
- method: 'POST',
597
- headers: { 'Content-Type': 'application/json' },
598
- body: JSON.stringify(data)
599
- });
600
- saveStatus('saved');
601
- lastSaved(new Date());
602
- } catch (error) {
603
- saveStatus('error');
604
- console.error('Auto-save failed:', error);
605
- }
606
- }, 1000);
607
-
608
- return () => clearTimeout(saveTimeout);
609
- });
610
-
611
- // UI feedback
612
- const statusMessage = $(() => {
613
- const status = saveStatus();
614
- const saved = lastSaved();
615
-
616
- if (status === 'saving') return 'Saving...';
617
- if (status === 'error') return 'Save failed';
618
- if (status === 'saved' && saved) {
619
- return `Last saved: ${saved().toLocaleTimeString()}`;
620
- }
621
- return '';
622
- });
623
- ```
624
-
625
- ### Real-time Search with Debounce
626
-
627
- ```javascript
628
- import { $ } from 'sigpro';
629
-
630
- const searchInput = $('');
631
- const searchResults = $([]);
632
- const searchStatus = $('idle'); // 'idle', 'searching', 'results', 'no-results', 'error'
633
- let searchTimeout;
634
- let abortController = null;
635
-
636
- $.effect(() => {
637
- const query = searchInput().trim();
638
-
639
- // Clear previous timeout
640
- clearTimeout(searchTimeout);
641
-
642
- // Cancel previous request
643
- if (abortController) {
644
- abortController.abort();
645
- abortController = null;
646
- }
647
-
648
- // Don't search for short queries
649
- if (query.length < 2) {
650
- searchResults([]);
651
- searchStatus('idle');
652
- return;
653
- }
654
-
655
- searchStatus('searching');
656
-
657
- // Debounce search
658
- searchTimeout = setTimeout(async () => {
659
- abortController = new AbortController();
660
-
661
- try {
662
- const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
663
- signal: abortController.signal
664
- });
665
-
666
- const data = await response.json();
667
-
668
- if (!abortController.signal.aborted) {
669
- searchResults(data);
670
- searchStatus(data.length ? 'results' : 'no-results');
671
- abortController = null;
672
- }
673
- } catch (error) {
674
- if (error.name !== 'AbortError') {
675
- console.error('Search failed:', error);
676
- searchStatus('error');
677
- }
678
- }
679
- }, 300);
680
-
681
- return () => {
682
- clearTimeout(searchTimeout);
683
- if (abortController) {
684
- abortController.abort();
685
- abortController = null;
686
- }
687
- };
688
- });
689
- ```
690
-
691
- ### Analytics Tracking
692
-
693
- ```javascript
694
- import { $ } from 'sigpro';
695
-
696
- // Analytics configuration
697
- const analyticsEnabled = $(true);
698
- const currentPage = $('/');
699
- const userProperties = $({});
700
-
701
- // Track page views
702
- $.effect(() => {
703
- if (!analyticsEnabled()) return;
704
-
705
- const page = currentPage();
706
- const properties = userProperties();
707
-
708
- console.log('Track page view:', page, properties);
709
-
710
- // Send to analytics
711
- gtag('config', 'GA-MEASUREMENT-ID', {
712
- page_path: page,
713
- ...properties
714
- });
715
- });
716
-
717
- // Track user interactions
718
- const trackEvent = (eventName, properties = {}) => {
719
- $.effect(() => {
720
- if (!analyticsEnabled()) return;
721
-
722
- console.log('Track event:', eventName, properties);
723
- gtag('event', eventName, properties);
724
- });
725
- };
726
-
727
- // Usage
728
- currentPage('/dashboard');
729
- userProperties({ userId: 123, plan: 'premium' });
730
- trackEvent('button_click', { buttonId: 'signup' });
731
- ```
732
-
733
- ### Keyboard Shortcuts
734
-
735
- ```javascript
736
- import { $ } from 'sigpro';
737
-
738
- const shortcuts = $({
739
- 'ctrl+s': { handler: null, description: 'Save' },
740
- 'ctrl+z': { handler: null, description: 'Undo' },
741
- 'ctrl+shift+z': { handler: null, description: 'Redo' },
742
- 'escape': { handler: null, description: 'Close modal' }
743
- });
744
-
745
- const pressedKeys = new Set();
746
-
747
- $.effect(() => {
748
- const handleKeyDown = (e) => {
749
- const key = e.key.toLowerCase();
750
- const ctrl = e.ctrlKey ? 'ctrl+' : '';
751
- const shift = e.shiftKey ? 'shift+' : '';
752
- const alt = e.altKey ? 'alt+' : '';
753
- const meta = e.metaKey ? 'meta+' : '';
754
-
755
- const combo = `${ctrl}${shift}${alt}${meta}${key}`.replace(/\+$/, '');
756
-
757
- const shortcut = shortcuts()[combo];
758
- if (shortcut?.handler) {
759
- e.preventDefault();
760
- shortcut.handler();
761
- }
762
- };
763
-
764
- window.addEventListener('keydown', handleKeyDown);
765
-
766
- return () => window.removeEventListener('keydown', handleKeyDown);
767
- });
768
-
769
- // Register shortcuts
770
- shortcuts({
771
- ...shortcuts(),
772
- 'ctrl+s': {
773
- handler: () => saveDocument(),
774
- description: 'Save document'
775
- },
776
- 'ctrl+z': {
777
- handler: () => undo(),
778
- description: 'Undo'
779
- }
780
- });
781
- ```
782
-
783
- ### Infinite Scroll
784
-
785
- ```javascript
786
- import { $ } from 'sigpro';
787
-
788
- const posts = $([]);
789
- const page = $(1);
790
- const hasMore = $(true);
791
- const loading = $(false);
792
- let observer = null;
793
-
794
- // Load more posts
795
- const loadMore = async () => {
796
- if (loading() || !hasMore()) return;
797
-
798
- loading(true);
799
- try {
800
- const response = await fetch(`/api/posts?page=${page()}`);
801
- const newPosts = await response.json();
802
-
803
- if (newPosts.length === 0) {
804
- hasMore(false);
805
- } else {
806
- posts([...posts(), ...newPosts]);
807
- page(p => p + 1);
808
- }
809
- } finally {
810
- loading(false);
811
- }
812
- };
813
-
814
- // Setup intersection observer for infinite scroll
815
- $.effect(() => {
816
- const sentinel = document.getElementById('sentinel');
817
- if (!sentinel) return;
818
-
819
- observer = new IntersectionObserver(
820
- (entries) => {
821
- if (entries[0].isIntersecting && !loading() && hasMore()) {
822
- loadMore();
823
- }
824
- },
825
- { threshold: 0.1 }
826
- );
827
-
828
- observer.observe(sentinel);
829
-
830
- return () => {
831
- if (observer) {
832
- observer.disconnect();
833
- observer = null;
834
- }
835
- };
836
- });
837
-
838
- // Initial load
839
- loadMore();
840
- ```
841
-
842
- ## 📊 Performance Considerations
843
-
844
- | Pattern | Performance Impact | Best Practice |
845
- |---------|-------------------|---------------|
846
- | Multiple signal reads | O(n) per effect | Group related signals |
847
- | Deep object access | Minimal | Use computed signals |
848
- | Large arrays | O(n) for iteration | Memoize with computed |
849
- | Frequent updates | Batched | Let batching work |
850
- | Heavy computations | Blocking | Use Web Workers |
851
-
852
- ## 🎯 Best Practices
853
-
854
- ### 1. Keep Effects Focused
855
-
856
- ```javascript
857
- // ❌ Avoid doing too much in one effect
858
- $.effect(() => {
859
- updateUI(count()); // UI update
860
- saveToStorage(count()); // Storage
861
- sendAnalytics(count()); // Analytics
862
- validate(count()); // Validation
863
- });
864
-
865
- // ✅ Split into focused effects
866
- $.effect(() => updateUI(count()));
867
- $.effect(() => saveToStorage(count()));
868
- $.effect(() => sendAnalytics(count()));
869
- $.effect(() => validate(count()));
870
- ```
871
-
872
- ### 2. Always Clean Up
873
-
874
- ```javascript
875
- // ❌ Missing cleanup
876
- $.effect(() => {
877
- const timer = setInterval(() => {}, 1000);
878
- // Memory leak!
879
- });
880
-
881
- // ✅ Proper cleanup
882
- $.effect(() => {
883
- const timer = setInterval(() => {}, 1000);
884
- return () => clearInterval(timer);
885
- });
886
- ```
887
-
888
- ### 3. Avoid Writing to Signals in Effects
889
-
890
- ```javascript
891
- import { $ } from 'sigpro';
892
-
893
- const a = $(1);
894
- const b = $(2);
895
-
896
- // ❌ Avoid - can cause loops
897
- $.effect(() => {
898
- a(b()); // Writing to a while reading b
899
- });
900
-
901
- // ✅ Use computed signals instead
902
- const sum = $(() => a() + b());
903
- ```
904
-
905
- ### 4. Use Conditional Logic Carefully
906
-
907
- ```javascript
908
- // ❌ Condition affects dependency tracking
909
- $.effect(() => {
910
- if (condition()) {
911
- console.log(a()); // Only tracks a when condition is true
912
- }
913
- });
914
-
915
- // ✅ Track all dependencies explicitly
916
- $.effect(() => {
917
- const cond = condition(); // Track condition
918
- if (cond) {
919
- console.log(a()); // Track a
920
- }
921
- });
922
- ```
923
-
924
- ### 5. Memoize Expensive Computations
925
-
926
- ```javascript
927
- import { $ } from 'sigpro';
928
-
929
- const items = $([]);
930
-
931
- // ❌ Expensive computation runs on every effect
932
- $.effect(() => {
933
- const total = items().reduce((sum, i) => sum + i.price, 0);
934
- updateTotal(total);
935
- });
936
-
937
- // ✅ Memoize with computed signal
938
- const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
939
- $.effect(() => updateTotal(total()));
940
- ```
941
-
942
- ## 🔍 Debugging Effects
943
-
944
- ### Logging Effect Runs
945
-
946
- ```javascript
947
- import { $ } from 'sigpro';
948
-
949
- const withLogging = (effectFn, name) => {
950
- return $.effect(() => {
951
- console.log(`[${name}] Running...`);
952
- const start = performance.now();
953
-
954
- const result = effectFn();
955
-
956
- const duration = performance.now() - start;
957
- console.log(`[${name}] Completed in ${duration.toFixed(2)}ms`);
958
-
959
- return result;
960
- });
961
- };
962
-
963
- // Usage
964
- withLogging(() => {
965
- console.log('Count:', count());
966
- }, 'count-effect');
967
- ```
968
-
969
- ### Effect Inspector
970
-
971
- ```javascript
972
- import { $ } from 'sigpro';
973
-
974
- const createEffectInspector = () => {
975
- const effects = new Map();
976
- let id = 0;
977
-
978
- const trackedEffect = (fn, name = `effect-${++id}`) => {
979
- const info = {
980
- name,
981
- runs: 0,
982
- lastRun: null,
983
- duration: 0,
984
- dependencies: new Set()
985
- };
986
-
987
- const wrapped = () => {
988
- info.runs++;
989
- info.lastRun = new Date();
990
- const start = performance.now();
991
-
992
- const result = fn();
993
-
994
- info.duration = performance.now() - start;
995
- return result;
996
- };
997
-
998
- const stop = $.effect(wrapped);
999
- effects.set(stop, info);
1000
-
1001
- return stop;
1002
- };
1003
-
1004
- const getReport = () => {
1005
- const report = {};
1006
- effects.forEach((info, stop) => {
1007
- report[info.name] = {
1008
- runs: info.runs,
1009
- lastRun: info.lastRun,
1010
- avgDuration: info.duration / info.runs
1011
- };
1012
- });
1013
- return report;
1014
- };
1015
-
1016
- return { trackedEffect, getReport };
1017
- };
1018
-
1019
- // Usage
1020
- const inspector = createEffectInspector();
1021
- inspector.trackedEffect(() => {
1022
- console.log('Count:', count());
1023
- }, 'counter-effect');
1024
- ```
1025
-
1026
- ## 📊 Summary
1027
-
1028
- | Feature | Description |
1029
- |---------|-------------|
1030
- | **Automatic Tracking** | Dependencies tracked automatically |
1031
- | **Cleanup Functions** | Return function to clean up resources |
1032
- | **Batch Updates** | Multiple changes batched in microtask |
1033
- | **Manual Stop** | Can stop effects with returned function |
1034
- | **Nested Effects** | Effects can contain other effects |
1035
- | **Auto-cleanup** | Effects in pages/components auto-cleaned |
1036
-
1037
- ---
1038
-
1039
- > **Pro Tip:** Effects are the perfect place for side effects like DOM updates, data fetching, and subscriptions. Keep them focused and always clean up resources!