@signaltree/core 4.0.6 → 4.0.7
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.
- package/README.md +1469 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/JBorgia/signaltree/main/apps/demo/public/signaltree.svg" alt="SignalTree Logo" width="60" height="60" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# SignalTree Core
|
|
6
|
+
|
|
7
|
+
Foundation package for SignalTree. Provides recursive typing, deep nesting support, and strong performance.
|
|
8
|
+
|
|
9
|
+
## What is @signaltree/core?
|
|
10
|
+
|
|
11
|
+
SignalTree Core is a lightweight package that provides:
|
|
12
|
+
|
|
13
|
+
- Recursive typing with deep nesting and accurate type inference
|
|
14
|
+
- Fast operations with sub‑millisecond measurements at 5–20+ levels
|
|
15
|
+
- Strong TypeScript safety across nested structures
|
|
16
|
+
- Memory efficiency via structural sharing and lazy signals
|
|
17
|
+
- Small API surface with zero-cost abstractions
|
|
18
|
+
- Compact bundle size suited for production
|
|
19
|
+
|
|
20
|
+
### Callable leaf signals (DX sugar only)
|
|
21
|
+
|
|
22
|
+
SignalTree provides TypeScript support for callable syntax on leaf signals as developer experience sugar:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// TypeScript accepts this syntax (with proper tooling):
|
|
26
|
+
tree.$.name('Jane'); // Set value
|
|
27
|
+
tree.$.count((n) => n + 1); // Update with function
|
|
28
|
+
|
|
29
|
+
// At build time, transforms convert to:
|
|
30
|
+
tree.$.name.set('Jane'); // Direct Angular signal API
|
|
31
|
+
tree.$.count.update((n) => n + 1); // Direct Angular signal API
|
|
32
|
+
|
|
33
|
+
// Reading always works directly:
|
|
34
|
+
const name = tree.$.name(); // No transform needed
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Key Points:**
|
|
38
|
+
|
|
39
|
+
- **Zero runtime overhead**: No Proxy wrappers or runtime hooks
|
|
40
|
+
- **Build-time only**: AST transform converts callable syntax to direct `.set/.update` calls
|
|
41
|
+
- **Optional**: Use `@signaltree/callable-syntax` transform or stick with direct `.set/.update`
|
|
42
|
+
- **Type-safe**: Full TypeScript support via module augmentation
|
|
43
|
+
|
|
44
|
+
**Function-valued leaves:**
|
|
45
|
+
When a leaf stores a function as its value, use direct `.set(fn)` to assign. Callable `sig(fn)` is treated as an updater.
|
|
46
|
+
|
|
47
|
+
**Setup:**
|
|
48
|
+
Install `@signaltree/callable-syntax` and configure your build tool to apply the transform. Without the transform, use `.set/.update` directly.
|
|
49
|
+
|
|
50
|
+
### Measuring performance and size
|
|
51
|
+
|
|
52
|
+
Performance and bundle size vary by app shape, build tooling, device, and runtime. To get meaningful results for your environment:
|
|
53
|
+
|
|
54
|
+
- Use the **Benchmark Orchestrator** in the demo app to run calibrated, scenario-based benchmarks across supported libraries with **real-world frequency weighting**. It applies research-based multipliers derived from 40,000+ developer surveys and GitHub analysis, reports statistical summaries (median/p95/p99/stddev), alternates runs to reduce bias, and can export CSV/JSON. When available, memory usage is also reported.
|
|
55
|
+
- Use the bundle analysis scripts in `scripts/` to measure your min+gz sizes. Sizes are approximate and depend on tree-shaking and configuration.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
### Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @signaltree/core
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Deep nesting example
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { signalTree } from '@signaltree/core';
|
|
69
|
+
|
|
70
|
+
// Strong type inference at deep nesting levels
|
|
71
|
+
const tree = signalTree({
|
|
72
|
+
enterprise: {
|
|
73
|
+
divisions: {
|
|
74
|
+
technology: {
|
|
75
|
+
departments: {
|
|
76
|
+
engineering: {
|
|
77
|
+
teams: {
|
|
78
|
+
frontend: {
|
|
79
|
+
projects: {
|
|
80
|
+
signaltree: {
|
|
81
|
+
releases: {
|
|
82
|
+
v1: {
|
|
83
|
+
features: {
|
|
84
|
+
recursiveTyping: {
|
|
85
|
+
validation: {
|
|
86
|
+
tests: {
|
|
87
|
+
extreme: {
|
|
88
|
+
depth: 15,
|
|
89
|
+
typeInference: true,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Type inference at deep nesting levels
|
|
109
|
+
const depth = tree.$.enterprise.divisions.technology.departments.engineering.teams.frontend.projects.signaltree.releases.v1.features.recursiveTyping.validation.tests.extreme.depth();
|
|
110
|
+
console.log(`Depth: ${depth}`);
|
|
111
|
+
|
|
112
|
+
// Type-safe updates at unlimited depth
|
|
113
|
+
tree.$.enterprise.divisions.technology.departments.engineering.teams.frontend.projects.signaltree.releases.v1.features.recursiveTyping.validation.tests.extreme.depth(25); // Perfect type safety!
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Basic usage
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { signalTree } from '@signaltree/core';
|
|
120
|
+
|
|
121
|
+
// Create a simple tree
|
|
122
|
+
const tree = signalTree({
|
|
123
|
+
count: 0,
|
|
124
|
+
message: 'Hello World',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Read values (these are Angular signals)
|
|
128
|
+
console.log(tree.$.count()); // 0
|
|
129
|
+
console.log(tree.$.message()); // 'Hello World'
|
|
130
|
+
|
|
131
|
+
// Update values
|
|
132
|
+
tree.$.count(5);
|
|
133
|
+
tree.$.message('Updated!');
|
|
134
|
+
|
|
135
|
+
// Use in an Angular component
|
|
136
|
+
@Component({
|
|
137
|
+
template: ` <div>Count: {{ tree.$.count() }}</div>
|
|
138
|
+
<div>Message: {{ tree.$.message() }}</div>
|
|
139
|
+
<button (click)="increment()">+1</button>`,
|
|
140
|
+
})
|
|
141
|
+
class SimpleComponent {
|
|
142
|
+
tree = tree;
|
|
143
|
+
|
|
144
|
+
increment() {
|
|
145
|
+
this.tree.$.count((n) => n + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Intermediate usage (nested state)
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Create hierarchical state
|
|
154
|
+
const tree = signalTree({
|
|
155
|
+
user: {
|
|
156
|
+
name: 'John Doe',
|
|
157
|
+
email: 'john@example.com',
|
|
158
|
+
preferences: {
|
|
159
|
+
theme: 'dark',
|
|
160
|
+
notifications: true,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
ui: {
|
|
164
|
+
loading: false,
|
|
165
|
+
errors: [] as string[],
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Access nested signals with full type safety
|
|
170
|
+
tree.$.user.name('Jane Doe');
|
|
171
|
+
tree.$.user.preferences.theme('light');
|
|
172
|
+
tree.$.ui.loading(true);
|
|
173
|
+
|
|
174
|
+
// Computed values from nested state
|
|
175
|
+
const userDisplayName = computed(() => {
|
|
176
|
+
const user = tree.$.user();
|
|
177
|
+
return `${user.name} (${user.email})`;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Effects that respond to changes
|
|
181
|
+
effect(() => {
|
|
182
|
+
if (tree.$.ui.loading()) {
|
|
183
|
+
console.log('Loading started...');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Reactive computations with computed()
|
|
189
|
+
|
|
190
|
+
SignalTree works seamlessly with Angular's `computed()` for creating efficient reactive computations. These computations automatically update when their dependencies change and are memoized for optimal performance.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { computed, effect } from '@angular/core';
|
|
194
|
+
import { signalTree } from '@signaltree/core';
|
|
195
|
+
|
|
196
|
+
const tree = signalTree({
|
|
197
|
+
users: [
|
|
198
|
+
{ id: '1', name: 'Alice', active: true, role: 'admin' },
|
|
199
|
+
{ id: '2', name: 'Bob', active: false, role: 'user' },
|
|
200
|
+
{ id: '3', name: 'Charlie', active: true, role: 'user' },
|
|
201
|
+
],
|
|
202
|
+
filters: {
|
|
203
|
+
showActive: true,
|
|
204
|
+
role: 'all' as 'all' | 'admin' | 'user',
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Basic computed - automatically memoized
|
|
209
|
+
const userCount = computed(() => tree.$.users().length);
|
|
210
|
+
|
|
211
|
+
// Complex filtering computation
|
|
212
|
+
const filteredUsers = computed(() => {
|
|
213
|
+
const users = tree.$.users();
|
|
214
|
+
const filters = tree.$.filters();
|
|
215
|
+
|
|
216
|
+
return users.filter((user) => {
|
|
217
|
+
if (filters.showActive && !user.active) return false;
|
|
218
|
+
if (filters.role !== 'all' && user.role !== filters.role) return false;
|
|
219
|
+
return true;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Derived computation from other computed values
|
|
224
|
+
const activeAdminCount = computed(() => filteredUsers().filter((user) => user.role === 'admin' && user.active).length);
|
|
225
|
+
|
|
226
|
+
// Performance-critical computation with complex logic
|
|
227
|
+
const userStatistics = computed(() => {
|
|
228
|
+
const users = tree.$.users();
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
total: users.length,
|
|
232
|
+
active: users.filter((u) => u.active).length,
|
|
233
|
+
admins: users.filter((u) => u.role === 'admin').length,
|
|
234
|
+
averageNameLength: users.reduce((acc, u) => acc + u.name.length, 0) / users.length,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Dynamic computed functions (factory pattern)
|
|
239
|
+
const userById = (id: string) => computed(() => tree.$.users().find((user) => user.id === id));
|
|
240
|
+
|
|
241
|
+
// Usage in effects
|
|
242
|
+
effect(() => {
|
|
243
|
+
console.log(`Filtered users: ${filteredUsers().length}`);
|
|
244
|
+
console.log(`Statistics:`, userStatistics());
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Best Practices:
|
|
248
|
+
// 1. Use computed() for derived state that depends on signals
|
|
249
|
+
// 2. Keep computations pure - no side effects
|
|
250
|
+
// 3. Leverage automatic memoization for expensive operations
|
|
251
|
+
// 4. Chain computed values for complex transformations
|
|
252
|
+
// 5. Use factory functions for parameterized computations
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Performance optimization with memoization
|
|
256
|
+
|
|
257
|
+
When combined with `@signaltree/memoization`, computed values become even more powerful:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { withMemoization } from '@signaltree/core';
|
|
261
|
+
|
|
262
|
+
const tree = signalTree({
|
|
263
|
+
items: Array.from({ length: 10000 }, (_, i) => ({
|
|
264
|
+
id: i,
|
|
265
|
+
value: Math.random(),
|
|
266
|
+
category: `cat-${i % 10}`,
|
|
267
|
+
})),
|
|
268
|
+
}).with(withMemoization());
|
|
269
|
+
|
|
270
|
+
// Expensive computation - automatically cached by memoization enhancer
|
|
271
|
+
const expensiveComputation = computed(() => {
|
|
272
|
+
return tree.$.items()
|
|
273
|
+
.filter((item) => item.value > 0.5)
|
|
274
|
+
.reduce((acc, item) => acc + Math.sin(item.value * Math.PI), 0);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// The computation only runs when tree.$.items() actually changes
|
|
278
|
+
// Subsequent calls return cached result
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Advanced usage (full state tree)
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
interface AppState {
|
|
285
|
+
auth: {
|
|
286
|
+
user: User | null;
|
|
287
|
+
token: string | null;
|
|
288
|
+
isAuthenticated: boolean;
|
|
289
|
+
};
|
|
290
|
+
data: {
|
|
291
|
+
users: User[];
|
|
292
|
+
posts: Post[];
|
|
293
|
+
cache: Record<string, unknown>;
|
|
294
|
+
};
|
|
295
|
+
ui: {
|
|
296
|
+
theme: 'light' | 'dark';
|
|
297
|
+
sidebar: {
|
|
298
|
+
open: boolean;
|
|
299
|
+
width: number;
|
|
300
|
+
};
|
|
301
|
+
notifications: Notification[];
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const tree = signalTree<AppState>({
|
|
306
|
+
auth: {
|
|
307
|
+
user: null,
|
|
308
|
+
token: null,
|
|
309
|
+
isAuthenticated: false,
|
|
310
|
+
},
|
|
311
|
+
data: {
|
|
312
|
+
users: [],
|
|
313
|
+
posts: [],
|
|
314
|
+
cache: {},
|
|
315
|
+
},
|
|
316
|
+
ui: {
|
|
317
|
+
theme: 'light',
|
|
318
|
+
sidebar: { open: true, width: 250 },
|
|
319
|
+
notifications: [],
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Complex updates with type safety
|
|
324
|
+
tree((state) => ({
|
|
325
|
+
auth: {
|
|
326
|
+
...state.auth,
|
|
327
|
+
user: { id: '1', name: 'John' },
|
|
328
|
+
isAuthenticated: true,
|
|
329
|
+
},
|
|
330
|
+
ui: {
|
|
331
|
+
...state.ui,
|
|
332
|
+
notifications: [...state.ui.notifications, { id: '1', message: 'Welcome!', type: 'success' }],
|
|
333
|
+
},
|
|
334
|
+
}));
|
|
335
|
+
|
|
336
|
+
// Get entire state as plain object
|
|
337
|
+
const currentState = tree();
|
|
338
|
+
console.log('Current app state:', currentState);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Core features
|
|
342
|
+
|
|
343
|
+
### 1) Hierarchical signal trees
|
|
344
|
+
|
|
345
|
+
Create deeply nested reactive state with automatic type inference:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
const tree = signalTree({
|
|
349
|
+
user: { name: '', email: '' },
|
|
350
|
+
settings: { theme: 'dark', notifications: true },
|
|
351
|
+
todos: [] as Todo[],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Access nested signals with full type safety
|
|
355
|
+
tree.$.user.name(); // string signal
|
|
356
|
+
tree.$.settings.theme.set('light'); // type-checked value
|
|
357
|
+
tree.$.todos.update((todos) => [...todos, newTodo]); // array operations
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 2) TypeScript inference
|
|
361
|
+
|
|
362
|
+
SignalTree provides complete type inference without manual typing:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Automatic inference from initial state
|
|
366
|
+
const tree = signalTree({
|
|
367
|
+
count: 0, // Inferred as WritableSignal<number>
|
|
368
|
+
name: 'John', // Inferred as WritableSignal<string>
|
|
369
|
+
active: true, // Inferred as WritableSignal<boolean>
|
|
370
|
+
items: [] as Item[], // Inferred as WritableSignal<Item[]>
|
|
371
|
+
config: {
|
|
372
|
+
theme: 'dark' as const, // Inferred as WritableSignal<'dark'>
|
|
373
|
+
settings: {
|
|
374
|
+
nested: true, // Deep nesting maintained
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Type-safe access and updates
|
|
380
|
+
tree.$.count.set(5); // ✅ number
|
|
381
|
+
tree.$.count.set('invalid'); // ❌ Type error
|
|
382
|
+
tree.$.config.theme.set('light'); // ❌ Type error ('dark' const)
|
|
383
|
+
tree.$.config.settings.nested.set(false); // ✅ boolean
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 3) Manual state management
|
|
387
|
+
|
|
388
|
+
Core provides basic state updates - entity management requires `@signaltree/entities`:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
interface User {
|
|
392
|
+
id: string;
|
|
393
|
+
name: string;
|
|
394
|
+
email: string;
|
|
395
|
+
active: boolean;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const tree = signalTree({
|
|
399
|
+
users: [] as User[],
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Manual CRUD operations using core methods
|
|
403
|
+
function addUser(user: User) {
|
|
404
|
+
tree.$.users.update((users) => [...users, user]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function updateUser(id: string, updates: Partial<User>) {
|
|
408
|
+
tree.$.users.update((users) => users.map((user) => (user.id === id ? { ...user, ...updates } : user)));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function removeUser(id: string) {
|
|
412
|
+
tree.$.users.update((users) => users.filter((user) => user.id !== id));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Manual queries using computed signals
|
|
416
|
+
const userById = (id: string) => computed(() => tree.$.users().find((user) => user.id === id));
|
|
417
|
+
const activeUsers = computed(() => tree.$.users().filter((user) => user.active));
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 4) Manual async state management
|
|
421
|
+
|
|
422
|
+
Core provides basic state updates - async helpers are now provided via `@signaltree/middleware` helpers:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const tree = signalTree({
|
|
426
|
+
users: [] as User[],
|
|
427
|
+
loading: false,
|
|
428
|
+
error: null as string | null,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Manual async operation management
|
|
432
|
+
async function loadUsers() {
|
|
433
|
+
tree.$.loading.set(true);
|
|
434
|
+
tree.$.error.set(null);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const users = await api.getUsers();
|
|
438
|
+
tree.$.users.set(users);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
tree.$.error.set(error instanceof Error ? error.message : 'Unknown error');
|
|
441
|
+
} finally {
|
|
442
|
+
tree.$.loading.set(false);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Usage in component
|
|
447
|
+
@Component({
|
|
448
|
+
template: `
|
|
449
|
+
@if (tree.$.loading()) {
|
|
450
|
+
<div>Loading...</div>
|
|
451
|
+
} @else if (tree.$.error()) {
|
|
452
|
+
<div class="error">{{ tree.$.error() }}</div>
|
|
453
|
+
} @else { @for (user of tree.$.users(); track user.id) {
|
|
454
|
+
<user-card [user]="user" />
|
|
455
|
+
} }
|
|
456
|
+
<button (click)="loadUsers()">Refresh</button>
|
|
457
|
+
`,
|
|
458
|
+
})
|
|
459
|
+
class UsersComponent {
|
|
460
|
+
tree = tree;
|
|
461
|
+
loadUsers = loadUsers;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### 5) Performance considerations
|
|
466
|
+
|
|
467
|
+
### 6) Enhancers and composition
|
|
468
|
+
|
|
469
|
+
SignalTree Core provides the foundation, but its real power comes from composable enhancers. Each enhancer is a focused, tree-shakeable extension that adds specific functionality.
|
|
470
|
+
|
|
471
|
+
#### Available Enhancers
|
|
472
|
+
|
|
473
|
+
**Performance Enhancers:**
|
|
474
|
+
|
|
475
|
+
- `@signaltree/batching` - Batch updates to reduce recomputation and rendering
|
|
476
|
+
- `@signaltree/memoization` - Intelligent caching for expensive computations
|
|
477
|
+
|
|
478
|
+
**Data Management:**
|
|
479
|
+
|
|
480
|
+
- `@signaltree/entities` - Advanced CRUD operations for collections
|
|
481
|
+
- Async helpers are provided via `@signaltree/middleware` (createAsyncOperation / trackAsync)
|
|
482
|
+
- `@signaltree/serialization` - State persistence and SSR support
|
|
483
|
+
|
|
484
|
+
**Development Tools:**
|
|
485
|
+
|
|
486
|
+
- `@signaltree/devtools` - Redux DevTools integration
|
|
487
|
+
- `@signaltree/time-travel` - Undo/redo functionality
|
|
488
|
+
|
|
489
|
+
**Integration:**
|
|
490
|
+
|
|
491
|
+
- `@signaltree/ng-forms` - Angular Forms integration
|
|
492
|
+
- `@signaltree/middleware` - State interceptors and logging
|
|
493
|
+
- `@signaltree/presets` - Pre-configured common patterns
|
|
494
|
+
|
|
495
|
+
#### Composition Patterns
|
|
496
|
+
|
|
497
|
+
**Basic Enhancement:**
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { signalTree } from '@signaltree/core';
|
|
501
|
+
import { withBatching } from '@signaltree/core';
|
|
502
|
+
import { withDevTools } from '@signaltree/core';
|
|
503
|
+
|
|
504
|
+
// Apply enhancers in order
|
|
505
|
+
const tree = signalTree({ count: 0 }).with(
|
|
506
|
+
withBatching(), // Performance optimization
|
|
507
|
+
withDevTools() // Development tools
|
|
508
|
+
);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Performance-Focused Stack:**
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
import { signalTree, withBatching } from '@signaltree/core';
|
|
515
|
+
import { withMemoization } from '@signaltree/core';
|
|
516
|
+
import { withEntities } from '@signaltree/core';
|
|
517
|
+
|
|
518
|
+
const tree = signalTree({
|
|
519
|
+
products: [] as Product[],
|
|
520
|
+
ui: { loading: false },
|
|
521
|
+
}).with(
|
|
522
|
+
withBatching(), // Batch updates for optimal rendering
|
|
523
|
+
withMemoization(), // Cache expensive computations
|
|
524
|
+
withEntities() // Efficient CRUD operations
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Now supports advanced operations
|
|
528
|
+
tree.batchUpdate((state) => ({
|
|
529
|
+
products: [...state.products, newProduct],
|
|
530
|
+
ui: { loading: false },
|
|
531
|
+
}));
|
|
532
|
+
|
|
533
|
+
const products = tree.entities<Product>('products');
|
|
534
|
+
products.selectBy((p) => p.category === 'electronics');
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Full-Stack Application:**
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { withSerialization } from '@signaltree/core';
|
|
541
|
+
import { withTimeTravel } from '@signaltree/core';
|
|
542
|
+
|
|
543
|
+
const tree = signalTree({
|
|
544
|
+
user: null as User | null,
|
|
545
|
+
preferences: { theme: 'light' },
|
|
546
|
+
}).with(
|
|
547
|
+
// withAsync removed — API integration patterns are now covered by middleware helpers
|
|
548
|
+
withSerialization({
|
|
549
|
+
// Auto-save to localStorage
|
|
550
|
+
autoSave: true,
|
|
551
|
+
storage: 'localStorage',
|
|
552
|
+
}),
|
|
553
|
+
withTimeTravel() // Undo/redo support
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// Advanced async operations
|
|
557
|
+
const fetchUser = tree.asyncAction(async (id: string) => api.getUser(id), {
|
|
558
|
+
loadingKey: 'loading',
|
|
559
|
+
onSuccess: (user) => ({ user }),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Automatic state persistence
|
|
563
|
+
tree.$.preferences.theme('dark'); // Auto-saved
|
|
564
|
+
|
|
565
|
+
// Time travel
|
|
566
|
+
tree.undo(); // Revert changes
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### Enhancer Metadata & Ordering
|
|
570
|
+
|
|
571
|
+
Enhancers can declare metadata for automatic dependency resolution:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// Enhancers are automatically ordered based on requirements
|
|
575
|
+
const tree = signalTree(state).with(
|
|
576
|
+
withDevTools(), // Requires: core, provides: debugging
|
|
577
|
+
withBatching(), // Requires: core, provides: batching
|
|
578
|
+
withMemoization() // Requires: batching, provides: caching
|
|
579
|
+
);
|
|
580
|
+
// Automatically ordered: batching -> memoization -> devtools
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### Quick Start with Presets
|
|
584
|
+
|
|
585
|
+
For common patterns, use presets that combine multiple enhancers:
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import { ecommercePreset, dashboardPreset } from '@signaltree/core';
|
|
589
|
+
|
|
590
|
+
// E-commerce preset includes: entities, async, batching, serialization
|
|
591
|
+
const ecommerceTree = ecommercePreset({
|
|
592
|
+
products: [] as Product[],
|
|
593
|
+
cart: { items: [], total: 0 },
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Dashboard preset includes: batching, memoization, devtools
|
|
597
|
+
const dashboardTree = dashboardPreset({
|
|
598
|
+
metrics: { users: 0, revenue: 0 },
|
|
599
|
+
charts: { data: [] },
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
#### Core Stubs
|
|
604
|
+
|
|
605
|
+
SignalTree Core includes lightweight stubs for all enhancer methods. This allows you to write code that uses advanced features, and the methods will warn when the actual enhancer isn't installed:
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
const tree = signalTree({ users: [] as User[] });
|
|
609
|
+
|
|
610
|
+
// Works but warns: "Feature requires @signaltree/entities"
|
|
611
|
+
const users = tree.entities<User>('users');
|
|
612
|
+
users.add(newUser); // Warns: "Method requires @signaltree/entities"
|
|
613
|
+
|
|
614
|
+
// Install the enhancer to enable full functionality
|
|
615
|
+
const enhanced = tree.with(withEntities());
|
|
616
|
+
const realUsers = enhanced.entities<User>('users');
|
|
617
|
+
realUsers.add(newUser); // ✅ Works perfectly
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Core includes several performance optimizations:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
// Lazy signal creation (default)
|
|
624
|
+
const tree = signalTree(
|
|
625
|
+
{
|
|
626
|
+
largeObject: {
|
|
627
|
+
// Signals only created when accessed
|
|
628
|
+
level1: { level2: { level3: { data: 'value' } } },
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
useLazySignals: true, // Default: true
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// Custom equality function
|
|
637
|
+
const tree2 = signalTree(
|
|
638
|
+
{
|
|
639
|
+
items: [] as Item[],
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
useShallowComparison: false, // Deep equality (default)
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// Structural sharing for memory efficiency
|
|
647
|
+
tree.update((state) => ({
|
|
648
|
+
...state, // Reuses unchanged parts
|
|
649
|
+
newField: 'value',
|
|
650
|
+
}));
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
## Error handling examples
|
|
654
|
+
|
|
655
|
+
### Manual async error handling
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
const tree = signalTree({
|
|
659
|
+
data: null as ApiData | null,
|
|
660
|
+
loading: false,
|
|
661
|
+
error: null as Error | null,
|
|
662
|
+
retryCount: 0,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
async function loadDataWithRetry(attempt = 0) {
|
|
666
|
+
tree.$.loading.set(true);
|
|
667
|
+
tree.$.error.set(null);
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
const data = await api.getData();
|
|
671
|
+
tree.$.data.set(data);
|
|
672
|
+
tree.$.loading.set(false);
|
|
673
|
+
tree.$.retryCount.set(0);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (attempt < 3) {
|
|
676
|
+
// Retry logic
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
678
|
+
return loadDataWithRetry(attempt + 1);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
tree.$.loading.set(false);
|
|
682
|
+
tree.$.error.set(error instanceof Error ? error : new Error('Unknown error'));
|
|
683
|
+
tree.$.retryCount.update((count) => count + 1);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Error boundary component
|
|
688
|
+
@Component({
|
|
689
|
+
template: `
|
|
690
|
+
@if (tree.$.error()) {
|
|
691
|
+
<div class="error-boundary">
|
|
692
|
+
<h3>Something went wrong</h3>
|
|
693
|
+
<p>{{ tree.$.error()?.message }}</p>
|
|
694
|
+
<p>Attempts: {{ tree.$.retryCount() }}</p>
|
|
695
|
+
<button (click)="retry()">Retry</button>
|
|
696
|
+
<button (click)="clear()">Clear Error</button>
|
|
697
|
+
</div>
|
|
698
|
+
} @else {
|
|
699
|
+
<!-- Normal content -->
|
|
700
|
+
}
|
|
701
|
+
`,
|
|
702
|
+
})
|
|
703
|
+
class ErrorHandlingComponent {
|
|
704
|
+
tree = tree;
|
|
705
|
+
|
|
706
|
+
retry() {
|
|
707
|
+
loadDataWithRetry();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
clear() {
|
|
711
|
+
this.tree.$.error.set(null);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### State update error handling
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
const tree = signalTree({
|
|
720
|
+
items: [] as Item[],
|
|
721
|
+
validationErrors: [] as string[],
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Safe update with validation
|
|
725
|
+
function safeUpdateItem(id: string, updates: Partial<Item>) {
|
|
726
|
+
try {
|
|
727
|
+
tree.update((state) => {
|
|
728
|
+
const itemIndex = state.items.findIndex((item) => item.id === id);
|
|
729
|
+
if (itemIndex === -1) {
|
|
730
|
+
throw new Error(`Item with id ${id} not found`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const updatedItem = { ...state.items[itemIndex], ...updates };
|
|
734
|
+
|
|
735
|
+
// Validation
|
|
736
|
+
if (!updatedItem.name?.trim()) {
|
|
737
|
+
throw new Error('Item name is required');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const newItems = [...state.items];
|
|
741
|
+
newItems[itemIndex] = updatedItem;
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
items: newItems,
|
|
745
|
+
validationErrors: [], // Clear errors on success
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
} catch (error) {
|
|
749
|
+
tree.$.validationErrors.update((errors) => [...errors, error instanceof Error ? error.message : 'Unknown error']);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
## Package composition patterns
|
|
755
|
+
|
|
756
|
+
SignalTree Core is designed for modular composition. Start minimal and add features as needed.
|
|
757
|
+
|
|
758
|
+
### Basic Composition
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
import { signalTree } from '@signaltree/core';
|
|
762
|
+
|
|
763
|
+
// Core provides the foundation
|
|
764
|
+
const tree = signalTree({
|
|
765
|
+
users: [] as User[],
|
|
766
|
+
ui: { loading: false },
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Basic operations included in core
|
|
770
|
+
tree.$.users.set([...users, newUser]);
|
|
771
|
+
tree.$.ui.loading.set(true);
|
|
772
|
+
tree.effect(() => console.log('State changed'));
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Performance-Enhanced Composition
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
import { signalTree } from '@signaltree/core';
|
|
779
|
+
import { withBatching } from '@signaltree/core';
|
|
780
|
+
import { withMemoization } from '@signaltree/core';
|
|
781
|
+
|
|
782
|
+
// Add performance optimizations
|
|
783
|
+
const tree = signalTree({
|
|
784
|
+
products: [] as Product[],
|
|
785
|
+
filters: { category: '', search: '' },
|
|
786
|
+
}).with(
|
|
787
|
+
withBatching(), // Batch updates for optimal rendering
|
|
788
|
+
withMemoization() // Cache expensive computations
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Now supports batched updates
|
|
792
|
+
tree.batchUpdate((state) => ({
|
|
793
|
+
products: [...state.products, ...newProducts],
|
|
794
|
+
filters: { category: 'electronics', search: '' },
|
|
795
|
+
}));
|
|
796
|
+
|
|
797
|
+
// Expensive computations are automatically cached
|
|
798
|
+
const filteredProducts = computed(() => {
|
|
799
|
+
return tree.$.products()
|
|
800
|
+
.filter((p) => p.category.includes(tree.$.filters.category()))
|
|
801
|
+
.filter((p) => p.name.includes(tree.$.filters.search()));
|
|
802
|
+
});
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Data Management Composition
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
import { signalTree } from '@signaltree/core';
|
|
809
|
+
import { withEntities } from '@signaltree/core';
|
|
810
|
+
|
|
811
|
+
// Add data management capabilities (+2.77KB total)
|
|
812
|
+
const tree = signalTree({
|
|
813
|
+
users: [] as User[],
|
|
814
|
+
posts: [] as Post[],
|
|
815
|
+
ui: { loading: false, error: null },
|
|
816
|
+
}).with(
|
|
817
|
+
withEntities() // Advanced CRUD operations
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
// Advanced entity operations
|
|
821
|
+
const users = tree.entities<User>('users');
|
|
822
|
+
users.add(newUser);
|
|
823
|
+
users.selectBy((u) => u.active);
|
|
824
|
+
users.updateMany([{ id: '1', changes: { status: 'active' } }]);
|
|
825
|
+
|
|
826
|
+
// Powerful async actions
|
|
827
|
+
const fetchUsers = tree.asyncAction(async () => api.getUsers(), {
|
|
828
|
+
loadingKey: 'ui.loading',
|
|
829
|
+
errorKey: 'ui.error',
|
|
830
|
+
onSuccess: (users) => ({ users }),
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Full-Featured Development Composition
|
|
835
|
+
|
|
836
|
+
```typescript
|
|
837
|
+
import { signalTree } from '@signaltree/core';
|
|
838
|
+
import { withBatching } from '@signaltree/core';
|
|
839
|
+
import { withEntities } from '@signaltree/core';
|
|
840
|
+
import { withSerialization } from '@signaltree/core';
|
|
841
|
+
import { withTimeTravel } from '@signaltree/core';
|
|
842
|
+
import { withDevTools } from '@signaltree/core';
|
|
843
|
+
|
|
844
|
+
// Full development stack (example)
|
|
845
|
+
const tree = signalTree({
|
|
846
|
+
app: {
|
|
847
|
+
user: null as User | null,
|
|
848
|
+
preferences: { theme: 'light' },
|
|
849
|
+
data: { users: [], posts: [] },
|
|
850
|
+
},
|
|
851
|
+
}).with(
|
|
852
|
+
withBatching(), // Performance
|
|
853
|
+
withEntities(), // Data management
|
|
854
|
+
// withAsync removed — use middleware helpers for API integration
|
|
855
|
+
withSerialization({
|
|
856
|
+
// State persistence
|
|
857
|
+
autoSave: true,
|
|
858
|
+
storage: 'localStorage',
|
|
859
|
+
}),
|
|
860
|
+
withTimeTravel({
|
|
861
|
+
// Undo/redo
|
|
862
|
+
maxHistory: 50,
|
|
863
|
+
}),
|
|
864
|
+
withDevTools({
|
|
865
|
+
// Debug tools (dev only)
|
|
866
|
+
name: 'MyApp',
|
|
867
|
+
trace: true,
|
|
868
|
+
})
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
// Rich feature set available
|
|
872
|
+
const users = tree.entities<User>('app.data.users');
|
|
873
|
+
const fetchUser = tree.asyncAction(api.getUser);
|
|
874
|
+
tree.undo(); // Time travel
|
|
875
|
+
tree.save(); // Persistence
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
### Production-Ready Composition
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
import { signalTree } from '@signaltree/core';
|
|
882
|
+
import { withBatching } from '@signaltree/core';
|
|
883
|
+
import { withEntities } from '@signaltree/core';
|
|
884
|
+
import { withSerialization } from '@signaltree/core';
|
|
885
|
+
|
|
886
|
+
// Production build (no dev tools)
|
|
887
|
+
const tree = signalTree(initialState).with(
|
|
888
|
+
withBatching(), // Performance optimization
|
|
889
|
+
withEntities(), // Data management
|
|
890
|
+
// withAsync removed — use middleware helpers for API integration
|
|
891
|
+
withSerialization({
|
|
892
|
+
// User preferences
|
|
893
|
+
autoSave: true,
|
|
894
|
+
storage: 'localStorage',
|
|
895
|
+
key: 'app-v1.2.3',
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
// Clean, efficient, production-ready
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Conditional Enhancement
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
import { signalTree } from '@signaltree/core';
|
|
906
|
+
import { withDevTools } from '@signaltree/core';
|
|
907
|
+
import { withTimeTravel } from '@signaltree/core';
|
|
908
|
+
|
|
909
|
+
const isDevelopment = process.env['NODE_ENV'] === 'development';
|
|
910
|
+
|
|
911
|
+
// Conditional enhancement based on environment
|
|
912
|
+
const tree = signalTree(state).with(
|
|
913
|
+
withBatching(), // Always include performance
|
|
914
|
+
withEntities(), // Always include data management
|
|
915
|
+
...(isDevelopment
|
|
916
|
+
? [
|
|
917
|
+
// Development-only features
|
|
918
|
+
withDevTools(),
|
|
919
|
+
withTimeTravel(),
|
|
920
|
+
]
|
|
921
|
+
: [])
|
|
922
|
+
);
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Preset-Based Composition
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
import { ecommercePreset, dashboardPreset } from '@signaltree/core';
|
|
929
|
+
|
|
930
|
+
// Use presets for common patterns
|
|
931
|
+
const ecommerceTree = ecommercePreset({
|
|
932
|
+
products: [],
|
|
933
|
+
cart: { items: [], total: 0 },
|
|
934
|
+
user: null,
|
|
935
|
+
});
|
|
936
|
+
// Includes: entities, async, batching, serialization
|
|
937
|
+
|
|
938
|
+
const dashboardTree = dashboardPreset({
|
|
939
|
+
metrics: {},
|
|
940
|
+
charts: [],
|
|
941
|
+
filters: {},
|
|
942
|
+
});
|
|
943
|
+
// Includes: batching, memoization, devtools
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Measuring bundle size
|
|
947
|
+
|
|
948
|
+
Bundle sizes depend on your build, tree-shaking, and which enhancers you include. Use the scripts in `scripts/` to analyze min+gz for your configuration.
|
|
949
|
+
|
|
950
|
+
### Migration Strategy
|
|
951
|
+
|
|
952
|
+
Start with core and grow incrementally:
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
// Phase 1: Start with core
|
|
956
|
+
const tree = signalTree(state);
|
|
957
|
+
|
|
958
|
+
// Phase 2: Add performance when needed
|
|
959
|
+
const tree2 = tree.with(withBatching());
|
|
960
|
+
|
|
961
|
+
// Phase 3: Add data management for collections
|
|
962
|
+
const tree3 = tree2.with(withEntities());
|
|
963
|
+
|
|
964
|
+
// Phase 4: Add async for API integration
|
|
965
|
+
// withAsync removed — no explicit async enhancer; use middleware helpers instead
|
|
966
|
+
|
|
967
|
+
// Each phase is fully functional and production-ready
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
// Start minimal, add features as needed
|
|
972
|
+
let tree = signalTree(initialState);
|
|
973
|
+
|
|
974
|
+
if (isDevelopment) {
|
|
975
|
+
tree = tree.with(withDevTools());
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (needsPerformance) {
|
|
979
|
+
tree = tree.with(withBatching(), withMemoization());
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (needsTimeTravel) {
|
|
983
|
+
tree = tree.with(withTimeTravel());
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### Service-based pattern
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
@Injectable()
|
|
991
|
+
class AppStateService {
|
|
992
|
+
private tree = signalTree({
|
|
993
|
+
user: null as User | null,
|
|
994
|
+
settings: { theme: 'light' as const },
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// Expose specific parts
|
|
998
|
+
readonly user$ = this.tree.$.user;
|
|
999
|
+
readonly settings$ = this.tree.$.settings;
|
|
1000
|
+
|
|
1001
|
+
// Expose specific actions
|
|
1002
|
+
setUser(user: User) {
|
|
1003
|
+
this.tree.$.user.set(user);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
updateSettings(settings: Partial<Settings>) {
|
|
1007
|
+
this.tree.$.settings.update((current) => ({
|
|
1008
|
+
...current,
|
|
1009
|
+
...settings,
|
|
1010
|
+
}));
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// For advanced features, return the tree
|
|
1014
|
+
getTree() {
|
|
1015
|
+
return this.tree;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
## Measuring performance
|
|
1021
|
+
|
|
1022
|
+
For fair, reproducible measurements that reflect your app and hardware, use the **Benchmark Orchestrator** in the demo. It calibrates runs per scenario and library, applies **real-world frequency weighting** based on research analysis, reports robust statistics, and supports CSV/JSON export. Avoid copying fixed numbers from docs; results vary.
|
|
1023
|
+
|
|
1024
|
+
## Example
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
// Complete user management component
|
|
1028
|
+
@Component({
|
|
1029
|
+
template: `
|
|
1030
|
+
<div class="user-manager">
|
|
1031
|
+
<!-- User List -->
|
|
1032
|
+
<div class="user-list">
|
|
1033
|
+
@if (userTree.$.loading()) {
|
|
1034
|
+
<div class="loading">Loading users...</div>
|
|
1035
|
+
} @else if (userTree.$.error()) {
|
|
1036
|
+
<div class="error">
|
|
1037
|
+
{{ userTree.$.error() }}
|
|
1038
|
+
<button (click)="loadUsers()">Retry</button>
|
|
1039
|
+
</div>
|
|
1040
|
+
} @else { @for (user of users.selectAll()(); track user.id) {
|
|
1041
|
+
<div class="user-card">
|
|
1042
|
+
<h3>{{ user.name }}</h3>
|
|
1043
|
+
<p>{{ user.email }}</p>
|
|
1044
|
+
<button (click)="editUser(user)">Edit</button>
|
|
1045
|
+
<button (click)="deleteUser(user.id)">Delete</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
} }
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<!-- User Form -->
|
|
1051
|
+
<form (ngSubmit)="saveUser()" #form="ngForm">
|
|
1052
|
+
<input [(ngModel)]="userTree.$.form.name()" name="name" placeholder="Name" required />
|
|
1053
|
+
<input [(ngModel)]="userTree.$.form.email()" name="email" type="email" placeholder="Email" required />
|
|
1054
|
+
<button type="submit" [disabled]="form.invalid">{{ userTree.$.form.id() ? 'Update' : 'Create' }} User</button>
|
|
1055
|
+
<button type="button" (click)="clearForm()">Clear</button>
|
|
1056
|
+
</form>
|
|
1057
|
+
</div>
|
|
1058
|
+
`,
|
|
1059
|
+
})
|
|
1060
|
+
class UserManagerComponent implements OnInit {
|
|
1061
|
+
userTree = signalTree({
|
|
1062
|
+
users: [] as User[],
|
|
1063
|
+
loading: false,
|
|
1064
|
+
error: null as string | null,
|
|
1065
|
+
form: { id: '', name: '', email: '' },
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
constructor(private userService: UserService) {}
|
|
1069
|
+
|
|
1070
|
+
ngOnInit() {
|
|
1071
|
+
this.loadUsers();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async loadUsers() {
|
|
1075
|
+
this.userTree.$.loading.set(true);
|
|
1076
|
+
this.userTree.$.error.set(null);
|
|
1077
|
+
|
|
1078
|
+
try {
|
|
1079
|
+
const users = await this.userService.getUsers();
|
|
1080
|
+
this.userTree.$.users.set(users);
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
this.userTree.$.error.set(error instanceof Error ? error.message : 'Load failed');
|
|
1083
|
+
} finally {
|
|
1084
|
+
this.userTree.$.loading.set(false);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
editUser(user: User) {
|
|
1089
|
+
this.userTree.$.form.set(user);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async saveUser() {
|
|
1093
|
+
try {
|
|
1094
|
+
const form = this.userTree.$.form();
|
|
1095
|
+
if (form.id) {
|
|
1096
|
+
await this.userService.updateUser(form.id, form);
|
|
1097
|
+
this.updateUser(form.id, form);
|
|
1098
|
+
} else {
|
|
1099
|
+
const newUser = await this.userService.createUser(form);
|
|
1100
|
+
this.addUser(newUser);
|
|
1101
|
+
}
|
|
1102
|
+
this.clearForm();
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
this.userTree.$.error.set(error instanceof Error ? error.message : 'Save failed');
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private addUser(user: User) {
|
|
1109
|
+
this.userTree.$.users.update((users) => [...users, user]);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private updateUser(id: string, updates: Partial<User>) {
|
|
1113
|
+
this.userTree.$.users.update((users) => users.map((user) => (user.id === id ? { ...user, ...updates } : user)));
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
deleteUser(id: string) {
|
|
1117
|
+
if (confirm('Delete user?')) {
|
|
1118
|
+
this.removeUser(id);
|
|
1119
|
+
this.userService.deleteUser(id).catch((error) => {
|
|
1120
|
+
this.userTree.$.error.set(error.message);
|
|
1121
|
+
this.loadUsers(); // Reload on error
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private removeUser(id: string) {
|
|
1127
|
+
this.userTree.$.users.update((users) => users.filter((user) => user.id !== id));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
clearForm() {
|
|
1131
|
+
this.userTree.$.form.set({ id: '', name: '', email: '' });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
]
|
|
1137
|
+
|
|
1138
|
+
}
|
|
1139
|
+
}));
|
|
1140
|
+
|
|
1141
|
+
// Get entire state as plain object
|
|
1142
|
+
const currentState = tree.unwrap();
|
|
1143
|
+
console.log('Current app state:', currentState);
|
|
1144
|
+
|
|
1145
|
+
```
|
|
1146
|
+
});
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
## Core features
|
|
1150
|
+
|
|
1151
|
+
### Hierarchical signal trees
|
|
1152
|
+
|
|
1153
|
+
```typescript
|
|
1154
|
+
const tree = signalTree({
|
|
1155
|
+
user: { name: '', email: '' },
|
|
1156
|
+
settings: { theme: 'dark', notifications: true },
|
|
1157
|
+
todos: [] as Todo[],
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Access nested signals with full type safety
|
|
1161
|
+
tree.$.user.name(); // string
|
|
1162
|
+
tree.$.settings.theme.set('light');
|
|
1163
|
+
tree.$.todos.update((todos) => [...todos, newTodo]);
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### Manual entity management
|
|
1167
|
+
|
|
1168
|
+
```typescript
|
|
1169
|
+
// Manual CRUD operations
|
|
1170
|
+
const tree = signalTree({
|
|
1171
|
+
todos: [] as Todo[],
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
function addTodo(todo: Todo) {
|
|
1175
|
+
tree.$.todos.update((todos) => [...todos, todo]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function updateTodo(id: string, updates: Partial<Todo>) {
|
|
1179
|
+
tree.$.todos.update((todos) => todos.map((todo) => (todo.id === id ? { ...todo, ...updates } : todo)));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function removeTodo(id: string) {
|
|
1183
|
+
tree.$.todos.update((todos) => todos.filter((todo) => todo.id !== id));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Manual queries with computed signals
|
|
1187
|
+
const todoById = (id: string) => computed(() => tree.$.todos().find((todo) => todo.id === id));
|
|
1188
|
+
const allTodos = computed(() => tree.$.todos());
|
|
1189
|
+
const todoCount = computed(() => tree.$.todos().length);
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
### Manual async state management
|
|
1193
|
+
|
|
1194
|
+
```typescript
|
|
1195
|
+
async function loadUsers() {
|
|
1196
|
+
tree.$.loading.set(true);
|
|
1197
|
+
|
|
1198
|
+
try {
|
|
1199
|
+
const users = await api.getUsers();
|
|
1200
|
+
tree.$.users.set(users);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
tree.$.error.set(error instanceof Error ? error.message : 'Unknown error');
|
|
1203
|
+
} finally {
|
|
1204
|
+
tree.$.loading.set(false);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Use in components
|
|
1209
|
+
async function handleLoadUsers() {
|
|
1210
|
+
await loadUsers();
|
|
1211
|
+
}
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
### Reactive effects
|
|
1215
|
+
|
|
1216
|
+
```typescript
|
|
1217
|
+
// Create reactive effects
|
|
1218
|
+
tree.effect((state) => {
|
|
1219
|
+
console.log(`User: ${state.user.name}, Theme: ${state.settings.theme}`);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Manual subscriptions
|
|
1223
|
+
const unsubscribe = tree.subscribe((state) => {
|
|
1224
|
+
// Handle state changes
|
|
1225
|
+
});
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
## Core API reference
|
|
1229
|
+
|
|
1230
|
+
### signalTree()
|
|
1231
|
+
|
|
1232
|
+
```typescript
|
|
1233
|
+
const tree = signalTree(initialState, config?);
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
### Tree Methods
|
|
1237
|
+
|
|
1238
|
+
```typescript
|
|
1239
|
+
// State access
|
|
1240
|
+
tree.$.property(); // Read signal value
|
|
1241
|
+
tree.$.property.set(value); // Update signal
|
|
1242
|
+
tree.unwrap(); // Get plain object
|
|
1243
|
+
|
|
1244
|
+
// Tree operations
|
|
1245
|
+
tree.update(updater); // Update entire tree
|
|
1246
|
+
tree.effect(fn); // Create reactive effects
|
|
1247
|
+
tree.subscribe(fn); // Manual subscriptions
|
|
1248
|
+
tree.destroy(); // Cleanup resources
|
|
1249
|
+
|
|
1250
|
+
// Extended features (require additional packages)
|
|
1251
|
+
tree.entities<T>(key); // Entity helpers (requires @signaltree/entities)
|
|
1252
|
+
// tree.asyncAction(fn, config?) example removed — use middleware helpers instead
|
|
1253
|
+
tree.asyncAction(fn, config?); // Create async action
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
## Extending with optional packages
|
|
1257
|
+
|
|
1258
|
+
SignalTree Core can be extended with additional features:
|
|
1259
|
+
|
|
1260
|
+
```typescript
|
|
1261
|
+
import { signalTree } from '@signaltree/core';
|
|
1262
|
+
import { withBatching } from '@signaltree/core';
|
|
1263
|
+
import { withMemoization } from '@signaltree/core';
|
|
1264
|
+
import { withTimeTravel } from '@signaltree/core';
|
|
1265
|
+
|
|
1266
|
+
// Compose features using .with(...)
|
|
1267
|
+
const tree = signalTree(initialState).with(withBatching(), withMemoization(), withTimeTravel());
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
### Available extensions
|
|
1271
|
+
|
|
1272
|
+
All enhancers are now consolidated in the core package:
|
|
1273
|
+
|
|
1274
|
+
- **withBatching()** - Batch multiple updates for better performance
|
|
1275
|
+
- **withMemoization()** - Intelligent caching & performance optimization
|
|
1276
|
+
- **withMiddleware()** - Middleware system & state interceptors
|
|
1277
|
+
- **withEntities()** - Advanced entity management & CRUD operations
|
|
1278
|
+
- **withDevTools()** - Redux DevTools integration for debugging
|
|
1279
|
+
- **withTimeTravel()** - Undo/redo functionality & state history
|
|
1280
|
+
- **withSerialization()** - State persistence & SSR support
|
|
1281
|
+
- **ecommercePreset()** - Pre-configured setup for e-commerce applications
|
|
1282
|
+
- **dashboardPreset()** - Pre-configured setup for dashboard applications
|
|
1283
|
+
|
|
1284
|
+
## When to use core only
|
|
1285
|
+
|
|
1286
|
+
Perfect for:
|
|
1287
|
+
|
|
1288
|
+
- ✅ Simple to medium applications
|
|
1289
|
+
- ✅ Prototype and MVP development
|
|
1290
|
+
- ✅ When bundle size is critical
|
|
1291
|
+
- ✅ Learning signal-based state management
|
|
1292
|
+
- ✅ Applications with basic state needs
|
|
1293
|
+
|
|
1294
|
+
Consider extensions when you need:
|
|
1295
|
+
|
|
1296
|
+
- ⚡ Performance optimization (batching, memoization)
|
|
1297
|
+
- 🐛 Advanced debugging (devtools, time-travel)
|
|
1298
|
+
- 📝 Complex forms (ng-forms)
|
|
1299
|
+
- 🔧 Middleware patterns (middleware)
|
|
1300
|
+
|
|
1301
|
+
## Migration from NgRx
|
|
1302
|
+
|
|
1303
|
+
```typescript
|
|
1304
|
+
// Step 1: Create parallel tree
|
|
1305
|
+
const tree = signalTree(initialState);
|
|
1306
|
+
|
|
1307
|
+
// Step 2: Gradually migrate components
|
|
1308
|
+
// Before (NgRx)
|
|
1309
|
+
users$ = this.store.select(selectUsers);
|
|
1310
|
+
|
|
1311
|
+
// After (SignalTree)
|
|
1312
|
+
users = this.tree.$.users;
|
|
1313
|
+
|
|
1314
|
+
// Step 3: Replace effects with manual async operations
|
|
1315
|
+
// Before (NgRx)
|
|
1316
|
+
loadUsers$ = createEffect(() =>
|
|
1317
|
+
this.actions$.with(
|
|
1318
|
+
ofType(loadUsers),
|
|
1319
|
+
switchMap(() => this.api.getUsers())
|
|
1320
|
+
)
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
// After (SignalTree Core)
|
|
1324
|
+
async loadUsers() {
|
|
1325
|
+
try {
|
|
1326
|
+
const users = await api.getUsers();
|
|
1327
|
+
tree.$.users.set(users);
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
tree.$.error.set(error.message);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Or use middleware helpers for async actions (createAsyncOperation / trackAsync)
|
|
1334
|
+
loadUsers = tree.asyncAction(() => api.getUsers(), {
|
|
1335
|
+
onSuccess: (users) => ({ users }),
|
|
1336
|
+
});
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
## Examples
|
|
1340
|
+
|
|
1341
|
+
### Simple Counter
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
const counter = signalTree({ count: 0 });
|
|
1345
|
+
|
|
1346
|
+
// In component
|
|
1347
|
+
@Component({
|
|
1348
|
+
template: ` <button (click)="increment()">{{ counter.$.count() }}</button> `,
|
|
1349
|
+
})
|
|
1350
|
+
class CounterComponent {
|
|
1351
|
+
counter = counter;
|
|
1352
|
+
|
|
1353
|
+
increment() {
|
|
1354
|
+
this.counter.$.count.update((n) => n + 1);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### User Management
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
const userTree = signalTree({
|
|
1363
|
+
users: [] as User[],
|
|
1364
|
+
loading: false,
|
|
1365
|
+
error: null as string | null,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
async function loadUsers() {
|
|
1369
|
+
userTree.$.loading.set(true);
|
|
1370
|
+
try {
|
|
1371
|
+
const users = await api.getUsers();
|
|
1372
|
+
userTree.$.users.set(users);
|
|
1373
|
+
userTree.$.error.set(null);
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
userTree.$.error.set(error instanceof Error ? error.message : 'Load failed');
|
|
1376
|
+
} finally {
|
|
1377
|
+
userTree.$.loading.set(false);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function addUser(user: User) {
|
|
1382
|
+
userTree.$.users.update((users) => [...users, user]);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// In component
|
|
1386
|
+
@Component({
|
|
1387
|
+
template: `
|
|
1388
|
+
@if (userTree.$.loading()) {
|
|
1389
|
+
<spinner />
|
|
1390
|
+
} @else { @for (user of userTree.$.users(); track user.id) {
|
|
1391
|
+
<user-card [user]="user" />
|
|
1392
|
+
} }
|
|
1393
|
+
`,
|
|
1394
|
+
})
|
|
1395
|
+
class UsersComponent {
|
|
1396
|
+
userTree = userTree;
|
|
1397
|
+
|
|
1398
|
+
ngOnInit() {
|
|
1399
|
+
loadUsers();
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
addUser(userData: Partial<User>) {
|
|
1403
|
+
const newUser = { id: crypto.randomUUID(), ...userData } as User;
|
|
1404
|
+
addUser(newUser);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
## Available extension packages
|
|
1410
|
+
|
|
1411
|
+
All enhancers are now consolidated in the core package. The following features are available directly from `@signaltree/core`:
|
|
1412
|
+
|
|
1413
|
+
### Performance & Optimization
|
|
1414
|
+
|
|
1415
|
+
- **withBatching()** (+1.27KB gzipped) - Batch multiple updates for better performance
|
|
1416
|
+
- **withMemoization()** (+2.33KB gzipped) - Intelligent caching & performance optimization
|
|
1417
|
+
|
|
1418
|
+
### Advanced Features
|
|
1419
|
+
|
|
1420
|
+
- **withMiddleware()** (+1.89KB gzipped) - Middleware system & state interceptors
|
|
1421
|
+
- **withEntities()** (+0.97KB gzipped) - Enhanced CRUD operations & entity management
|
|
1422
|
+
|
|
1423
|
+
### Development Tools
|
|
1424
|
+
|
|
1425
|
+
- **withDevTools()** (+2.49KB gzipped) - Development tools & Redux DevTools integration
|
|
1426
|
+
- **withTimeTravel()** (+1.75KB gzipped) - Undo/redo functionality & state history
|
|
1427
|
+
|
|
1428
|
+
### Integration & Convenience
|
|
1429
|
+
|
|
1430
|
+
- **withSerialization()** (+0.84KB gzipped) - State persistence & SSR support
|
|
1431
|
+
- **ecommercePreset()** - Pre-configured setups for e-commerce applications
|
|
1432
|
+
- **dashboardPreset()** - Pre-configured setups for dashboard applications
|
|
1433
|
+
|
|
1434
|
+
### Quick Start with Extensions
|
|
1435
|
+
|
|
1436
|
+
All enhancers are now available from the core package:
|
|
1437
|
+
|
|
1438
|
+
```bash
|
|
1439
|
+
# Install only the core package - all features included
|
|
1440
|
+
npm install @signaltree/core
|
|
1441
|
+
|
|
1442
|
+
# Everything is available from @signaltree/core:
|
|
1443
|
+
import {
|
|
1444
|
+
signalTree,
|
|
1445
|
+
withBatching,
|
|
1446
|
+
withMemoization,
|
|
1447
|
+
withEntities,
|
|
1448
|
+
withDevTools,
|
|
1449
|
+
withTimeTravel,
|
|
1450
|
+
withSerialization,
|
|
1451
|
+
ecommercePreset,
|
|
1452
|
+
dashboardPreset
|
|
1453
|
+
} from '@signaltree/core';
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
## Links
|
|
1457
|
+
|
|
1458
|
+
- [SignalTree Documentation](https://signaltree.io)
|
|
1459
|
+
- [GitHub Repository](https://github.com/JBorgia/signaltree)
|
|
1460
|
+
- [NPM Package](https://www.npmjs.com/package/@signaltree/core)
|
|
1461
|
+
- [Interactive Examples](https://signaltree.io/examples)
|
|
1462
|
+
|
|
1463
|
+
## 📄 License
|
|
1464
|
+
|
|
1465
|
+
MIT License with AI Training Restriction - see the [LICENSE](../../LICENSE) file for details.
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
|
|
1469
|
+
**Ready to get started?** This core package provides everything you need for most applications. Add extensions only when you need them! 🚀
|