@zeix/cause-effect 0.17.0 → 0.17.2
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/.ai-context.md +26 -5
- package/.cursorrules +8 -3
- package/.github/copilot-instructions.md +13 -4
- package/CLAUDE.md +191 -262
- package/README.md +268 -420
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +5 -4
- package/archive/list.ts +21 -28
- package/archive/memo.ts +4 -2
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +6 -9
- package/index.dev.js +411 -220
- package/index.js +1 -1
- package/index.ts +25 -8
- package/package.json +1 -1
- package/src/classes/collection.ts +103 -77
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +90 -31
- package/src/classes/list.ts +39 -33
- package/src/classes/ref.ts +96 -0
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +31 -1
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -10
- package/test/collection.test.ts +383 -67
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +381 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +6 -5
- package/types/src/classes/collection.d.ts +27 -12
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +6 -6
- package/types/src/classes/ref.d.ts +48 -0
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +9 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -3
package/CLAUDE.md
CHANGED
|
@@ -6,21 +6,20 @@ Cause & Effect is a reactive state management library that implements the signal
|
|
|
6
6
|
|
|
7
7
|
### Core Philosophy
|
|
8
8
|
- **Granular Reactivity**: Changes propagate only to dependent computations, minimizing unnecessary work
|
|
9
|
-
- **Explicit Dependencies**:
|
|
10
|
-
- **
|
|
11
|
-
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints
|
|
9
|
+
- **Explicit Dependencies**: Dependencies are tracked through `.get()` calls, no hidden subscriptions
|
|
10
|
+
- **Type Safety First**: Comprehensive TypeScript support with strict generic constraints (`T extends {}`)
|
|
12
11
|
- **Performance Conscious**: Minimal overhead through efficient dependency tracking and batching
|
|
13
12
|
|
|
14
13
|
## Mental Model for Understanding the System
|
|
15
14
|
|
|
16
15
|
Think of signals as **observable cells** in a spreadsheet:
|
|
17
|
-
- **State signals** are
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
- **State signals** are input cells for primitive values and objects
|
|
17
|
+
- **Ref signals** are cells that reference external objects (DOM, Map, Set) requiring manual notification
|
|
18
|
+
- **Computed signals** are formula cells that automatically recalculate when dependencies change
|
|
19
|
+
- **Store signals** are structured tables where individual columns (properties) are reactive
|
|
20
|
+
- **List signals** are tables with stable row IDs that survive sorting and reordering
|
|
21
|
+
- **Collection signals** are read-only derived tables with item-level memoization
|
|
22
|
+
- **Effects** are event handlers that trigger side effects when cells change
|
|
24
23
|
|
|
25
24
|
## Architectural Deep Dive
|
|
26
25
|
|
|
@@ -45,72 +44,52 @@ interface MutableSignal<T extends {}> extends Signal<T> {
|
|
|
45
44
|
update(fn: (current: T) => T): void
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// Collection
|
|
49
|
-
interface Collection<T extends {}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// Collection interface - implemented by various collection types
|
|
48
|
+
interface Collection<T extends {}> {
|
|
49
|
+
get(): T[]
|
|
50
|
+
at(index: number): Signal<T> | undefined
|
|
51
|
+
byKey(key: string): Signal<T> | undefined
|
|
52
|
+
deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R>
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
The generic constraint `T extends {}` is crucial - it excludes `null` and `undefined` at the type level, preventing common runtime errors and making the API more predictable.
|
|
57
57
|
|
|
58
|
-
### Collection
|
|
59
|
-
|
|
60
|
-
Collection signals provide read-only derived array transformations with automatic memoization:
|
|
61
|
-
|
|
62
|
-
1. **Source Dependency**: Collections derive from Lists or other Collections
|
|
63
|
-
2. **Item-level Memoization**: Each item transformation is individually memoized
|
|
64
|
-
3. **Async Support**: Supports Promise-based transformations with cancellation
|
|
65
|
-
4. **Stable Keys**: Maintains the same stable key system as source Lists
|
|
66
|
-
5. **Chainable**: Collections can derive from other Collections for data pipelines
|
|
58
|
+
### Collection Architecture
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
- Individual items are Computed signals, not mutable signals
|
|
70
|
-
- Automatically handles source List changes (add, remove, sort)
|
|
71
|
-
- Supports both sync and async transformation callbacks
|
|
72
|
-
- Lazy evaluation - transformations only run when accessed
|
|
73
|
-
- Smart watcher management - only creates watchers when change listeners are active
|
|
74
|
-
- Order preservation through internal `#order` array synchronized with source
|
|
60
|
+
Collections are an interface implemented by different reactive array types:
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
- **Memory Efficiency**: Automatically cleans up computed signals when source items are removed
|
|
62
|
+
**DerivedCollection**: Read-only transformations of Lists or other Collections
|
|
63
|
+
- Item-level memoization with Computed signals
|
|
64
|
+
- Async support with automatic cancellation
|
|
65
|
+
- Chainable for data pipelines
|
|
81
66
|
|
|
82
|
-
|
|
67
|
+
**ElementCollection**: DOM element collections with MutationObserver
|
|
68
|
+
- Uses Ref signals for elements that change externally
|
|
69
|
+
- Watches attributes and childList mutations
|
|
70
|
+
- Stable keys for persistent element identity
|
|
83
71
|
|
|
84
|
-
|
|
72
|
+
Key patterns:
|
|
73
|
+
- Collections return arrays of values via `.get()`
|
|
74
|
+
- Individual items accessed as signals via `.at()` and `.byKey()`
|
|
75
|
+
- All collections support `.deriveCollection()` for chaining
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
2. **Nested Reactivity**: Objects within objects recursively become stores
|
|
88
|
-
3. **Array Support**: Arrays get special treatment with length tracking and efficient sorting
|
|
89
|
-
4. **Dynamic Properties**: Runtime addition/removal of properties with proper reactivity
|
|
77
|
+
### Store and List Architecture
|
|
90
78
|
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
- Handles edge cases like symbol properties and prototype chain
|
|
96
|
-
- For lists: maintains stable keys that persist through sorting, splicing, and reordering
|
|
97
|
-
- Provides specialized methods: `byKey()`, `keyAt()`, `indexOfKey()`, `splice()` for array manipulation
|
|
79
|
+
**Store signals** (`createStore`): Transform objects into reactive data structures
|
|
80
|
+
- Each property becomes its own signal via Proxy
|
|
81
|
+
- Built on `Composite` class for signal management
|
|
82
|
+
- Dynamic property addition/removal with proper reactivity
|
|
98
83
|
|
|
99
|
-
|
|
84
|
+
**List signals** (`new List`): Arrays with stable keys and reactive items
|
|
85
|
+
- Maintains stable keys that survive sorting and reordering
|
|
86
|
+
- Built on `Composite` class for consistent signal management
|
|
87
|
+
- Provides `byKey()`, `keyAt()`, `indexOfKey()` for key-based access
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
3. **Event System**: Emits granular notifications for add/change/remove operations
|
|
106
|
-
4. **Validation**: Enforces type constraints and prevents invalid mutations
|
|
107
|
-
5. **Performance**: Only creates signals when properties are first accessed or added
|
|
108
|
-
|
|
109
|
-
Key implementation details:
|
|
110
|
-
- Lazy signal creation for better memory usage
|
|
111
|
-
- Efficient batch updates through change detection
|
|
112
|
-
- Support for both object and array data structures
|
|
113
|
-
- Automatic cleanup when properties are removed
|
|
89
|
+
**Composite Architecture**: Shared foundation for Store and List
|
|
90
|
+
- `Map<string, Signal>` for property/item signals
|
|
91
|
+
- Hook system for granular add/change/remove notifications
|
|
92
|
+
- Lazy signal creation and automatic cleanup
|
|
114
93
|
|
|
115
94
|
### Computed Signal Memoization Strategy
|
|
116
95
|
|
|
@@ -121,6 +100,101 @@ Computed signals implement smart memoization:
|
|
|
121
100
|
- **Error Handling**: Preserves error states and prevents cascade failures
|
|
122
101
|
- **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
|
|
123
102
|
|
|
103
|
+
## Resource Management with Watch Hooks
|
|
104
|
+
|
|
105
|
+
All signals support the `watch` hook for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Basic watch hook pattern
|
|
109
|
+
const config = new State({ apiUrl: 'https://api.example.com' })
|
|
110
|
+
|
|
111
|
+
config.on('watch', () => {
|
|
112
|
+
console.log('Setting up API client...')
|
|
113
|
+
const client = new ApiClient(config.get().apiUrl)
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
console.log('Cleaning up API client...')
|
|
117
|
+
client.disconnect()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Resource is only created when effect runs
|
|
122
|
+
createEffect(() => {
|
|
123
|
+
console.log('API URL:', config.get().apiUrl) // Triggers hook
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Store Watch Hooks**: Monitor entire store or nested properties
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const database = createStore({ host: 'localhost', port: 5432 })
|
|
131
|
+
|
|
132
|
+
// Watch entire store - triggers when any property accessed
|
|
133
|
+
database.on('watch', () => {
|
|
134
|
+
console.log('Database connection needed')
|
|
135
|
+
const connection = connect(database.host.get(), database.port.get())
|
|
136
|
+
return () => connection.close()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Watch specific property
|
|
140
|
+
database.host.on('watch', () => {
|
|
141
|
+
console.log('Host property being watched')
|
|
142
|
+
return () => console.log('Host watching stopped')
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**List Watch Hooks**: Two-tier system for collection and item resources
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const items = new List(['apple', 'banana'])
|
|
150
|
+
|
|
151
|
+
// List-level resource (entire collection)
|
|
152
|
+
items.on('watch', () => {
|
|
153
|
+
console.log('List observer started')
|
|
154
|
+
return () => console.log('List observer stopped')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Item-level resource (individual items)
|
|
158
|
+
const firstItem = items.at(0)
|
|
159
|
+
firstItem.on('watch', () => {
|
|
160
|
+
console.log('First item being watched')
|
|
161
|
+
return () => console.log('First item watch stopped')
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Collection Watch Hooks**: Propagate to source List items
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const numbers = new List([1, 2, 3])
|
|
169
|
+
const doubled = numbers.deriveCollection(x => x * 2)
|
|
170
|
+
|
|
171
|
+
// Set up source item hook
|
|
172
|
+
numbers.at(0).on('watch', () => {
|
|
173
|
+
console.log('Source item accessed through collection')
|
|
174
|
+
return () => console.log('Source item no longer watched')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Accessing collection item triggers source item hook
|
|
178
|
+
createEffect(() => {
|
|
179
|
+
const value = doubled.at(0).get() // Triggers source item hook
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Practical Use Cases**:
|
|
184
|
+
- Event listeners that activate only when data is watched
|
|
185
|
+
- Network connections established on-demand
|
|
186
|
+
- Expensive computations that pause when not needed
|
|
187
|
+
- External subscriptions (WebSocket, Server-Sent Events)
|
|
188
|
+
- Database connections tied to data access patterns
|
|
189
|
+
|
|
190
|
+
**Hook Lifecycle**:
|
|
191
|
+
1. First effect accesses signal → `watch` hook callback executed
|
|
192
|
+
2. Hook callback can return cleanup function
|
|
193
|
+
3. Last effect stops watching → cleanup function called
|
|
194
|
+
4. New effect accesses signal → hook callback executed again
|
|
195
|
+
|
|
196
|
+
This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
|
|
197
|
+
|
|
124
198
|
## Advanced Patterns and Best Practices
|
|
125
199
|
|
|
126
200
|
### When to Use Each Signal Type
|
|
@@ -134,51 +208,41 @@ Computed signals implement smart memoization:
|
|
|
134
208
|
```typescript
|
|
135
209
|
const count = new State(0)
|
|
136
210
|
const theme = new State<'light' | 'dark'>('light')
|
|
137
|
-
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Ref Signals (`new Ref`)**:
|
|
214
|
+
- External objects that change outside the reactive system
|
|
215
|
+
- DOM elements, Map, Set, Date objects, third-party APIs
|
|
216
|
+
- Requires manual `.notify()` when external object changes
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
220
|
+
const cacheRef = new Ref(new Map())
|
|
221
|
+
// When external change occurs: cacheRef.notify()
|
|
138
222
|
```
|
|
139
223
|
|
|
140
224
|
**Store Signals (`createStore`)**:
|
|
141
225
|
- Objects where individual properties change independently
|
|
142
|
-
- Nested data structures
|
|
143
|
-
- Form state where fields update individually
|
|
144
|
-
- Configuration objects with multiple settings
|
|
145
226
|
|
|
146
227
|
```typescript
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
password: '',
|
|
150
|
-
errors: { email: null, password: null }
|
|
151
|
-
})
|
|
152
|
-
// form.email.set('user@example.com') // Only email subscribers react
|
|
228
|
+
const user = createStore({ name: 'Alice', email: 'alice@example.com' })
|
|
229
|
+
user.name.set('Bob') // Only name subscribers react
|
|
153
230
|
```
|
|
154
231
|
|
|
155
232
|
**List Signals (`new List`)**:
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
// Lists with stable keys
|
|
233
|
+
```typescript
|
|
159
234
|
const todoList = new List([
|
|
160
|
-
{ id: 'task1', text: 'Learn signals' }
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// Access by stable key instead of index
|
|
165
|
-
const firstTodo = todoList.byKey('task1')
|
|
166
|
-
firstTodo?.text.set('Learn signals deeply')
|
|
167
|
-
|
|
168
|
-
// Sort while maintaining stable references
|
|
169
|
-
todoList.sort((a, b) => a.text.localeCompare(b.text))
|
|
235
|
+
{ id: 'task1', text: 'Learn signals' }
|
|
236
|
+
], todo => todo.id)
|
|
237
|
+
const firstTodo = todoList.byKey('task1') // Access by stable key
|
|
170
238
|
```
|
|
171
239
|
|
|
172
|
-
**Collection Signals (`new
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Read-only derived arrays with memoization
|
|
176
|
-
const completedTodos = new Collection(todoList, todo =>
|
|
240
|
+
**Collection Signals (`new DerivedCollection`)**:
|
|
241
|
+
```typescript
|
|
242
|
+
const completedTodos = new DerivedCollection(todoList, todo =>
|
|
177
243
|
todo.completed ? { ...todo, status: 'done' } : null
|
|
178
244
|
)
|
|
179
|
-
|
|
180
|
-
// Async transformations with cancellation
|
|
181
|
-
const todoDetails = new Collection(todoList, async (todo, abort) => {
|
|
245
|
+
const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
|
|
182
246
|
const response = await fetch(`/todos/${todo.id}`, { signal: abort })
|
|
183
247
|
return { ...todo, details: await response.json() }
|
|
184
248
|
})
|
|
@@ -245,19 +309,13 @@ The library provides several layers of error handling:
|
|
|
245
309
|
4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
|
|
246
310
|
|
|
247
311
|
```typescript
|
|
248
|
-
//
|
|
312
|
+
// Error handling with resolve() and match()
|
|
249
313
|
const apiData = new Task(async (prev, abort) => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return response.json()
|
|
254
|
-
} catch (error) {
|
|
255
|
-
if (abort.aborted) return prev // Cancelled, not an error
|
|
256
|
-
throw error // Real error
|
|
257
|
-
}
|
|
314
|
+
const response = await fetch('/api/data', { signal: abort })
|
|
315
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
316
|
+
return response.json()
|
|
258
317
|
})
|
|
259
318
|
|
|
260
|
-
// Pattern matching for comprehensive state handling
|
|
261
319
|
createEffect(() => {
|
|
262
320
|
match(resolve({ apiData }), {
|
|
263
321
|
ok: ({ apiData }) => updateUI(apiData),
|
|
@@ -267,199 +325,70 @@ createEffect(() => {
|
|
|
267
325
|
})
|
|
268
326
|
```
|
|
269
327
|
|
|
270
|
-
### Performance Optimization
|
|
271
|
-
|
|
272
|
-
1. **Batching Updates**: Use `batchSignalWrites()` for multiple synchronous updates
|
|
273
|
-
2. **Selective Dependencies**: Structure computed signals to minimize dependencies
|
|
274
|
-
3. **Cleanup Management**: Properly dispose of effects to prevent memory leaks
|
|
275
|
-
4. **Shallow vs Deep Equality**: Use appropriate comparison strategies
|
|
328
|
+
### Performance Optimization
|
|
276
329
|
|
|
330
|
+
**Batching**: Use `batchSignalWrites()` for multiple updates
|
|
277
331
|
```typescript
|
|
278
|
-
// Create a user store
|
|
279
|
-
const user = createStore<{ id: number, name: string, email: string, age?: number }>({
|
|
280
|
-
id: 1,
|
|
281
|
-
name: 'John Doe',
|
|
282
|
-
email: 'john@example.com'
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// Batch multiple updates to prevent intermediate states
|
|
286
332
|
batchSignalWrites(() => {
|
|
287
|
-
user.name.set('Alice
|
|
288
|
-
user.age.set(30)
|
|
333
|
+
user.name.set('Alice')
|
|
289
334
|
user.email.set('alice@example.com')
|
|
290
|
-
}) //
|
|
291
|
-
|
|
292
|
-
// Optimize computed dependencies
|
|
293
|
-
const userDisplay = new Memo(() => {
|
|
294
|
-
const { name, email } = user.get() // Gets entire object, will rerun also if age changes
|
|
295
|
-
return `${name} <${email}>`
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// Better: more granular dependencies
|
|
299
|
-
const userDisplay = new Memo(() => {
|
|
300
|
-
return `${user.name.get()} <${user.email.get()}>` // Only depends on name/email
|
|
301
|
-
})
|
|
335
|
+
}) // Single effect trigger
|
|
302
336
|
```
|
|
303
337
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
- **Vanilla JS**: Direct DOM manipulation in effects
|
|
312
|
-
|
|
313
|
-
### Testing Strategies
|
|
314
|
-
- **Unit Testing**: Test signal logic in isolation
|
|
315
|
-
- **Integration Testing**: Test effect chains and async operations
|
|
316
|
-
- **Mock Testing**: Mock external dependencies in computed signals
|
|
317
|
-
- **Property Testing**: Use random inputs to verify invariants
|
|
318
|
-
|
|
319
|
-
### Debugging Techniques
|
|
320
|
-
- Use `resolve()` to inspect multiple signal states at once
|
|
321
|
-
- Add logging effects for debugging reactivity chains
|
|
322
|
-
- Use AbortSignal inspection for async operation debugging
|
|
323
|
-
- Leverage TypeScript for compile-time dependency analysis
|
|
338
|
+
**Granular Dependencies**: Structure computed signals to minimize dependencies
|
|
339
|
+
```typescript
|
|
340
|
+
// Bad: depends on entire user object
|
|
341
|
+
const display = new Memo(() => user.get().name + user.get().email)
|
|
342
|
+
// Good: only depends on specific properties
|
|
343
|
+
const display = new Memo(() => user.name.get() + user.email.get())
|
|
344
|
+
```
|
|
324
345
|
|
|
325
|
-
## Common Pitfalls
|
|
346
|
+
## Common Pitfalls
|
|
326
347
|
|
|
327
348
|
1. **Infinite Loops**: Don't update signals within their own computed callbacks
|
|
328
|
-
2. **
|
|
329
|
-
3. **
|
|
330
|
-
4. **
|
|
331
|
-
5. **Async Race Conditions**: Trust the automatic cancellation, don't fight it
|
|
349
|
+
2. **Memory Leaks**: Clean up effects when components unmount
|
|
350
|
+
3. **Over-reactivity**: Structure data to minimize unnecessary updates
|
|
351
|
+
4. **Async Race Conditions**: Trust automatic cancellation with AbortSignal
|
|
332
352
|
|
|
333
|
-
## Advanced
|
|
353
|
+
## Advanced Patterns
|
|
334
354
|
|
|
335
|
-
###
|
|
355
|
+
### Event Bus with Type Safety
|
|
336
356
|
```typescript
|
|
337
|
-
|
|
338
|
-
const todos = new List<Todo[]>([])
|
|
339
|
-
const completedCount = new Memo(() =>
|
|
340
|
-
todos.get().filter(todo => todo.completed).length
|
|
341
|
-
)
|
|
342
|
-
const remainingCount = new Memo(() =>
|
|
343
|
-
todos.get().length - completedCount.get()
|
|
344
|
-
)
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Reactive State Machines
|
|
348
|
-
```typescript
|
|
349
|
-
const state = new State<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
350
|
-
const canRetry = () => state.get() === 'error'
|
|
351
|
-
const isLoading = () => state.get() === 'loading'
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Cross-Component Communication
|
|
355
|
-
```typescript
|
|
356
|
-
// Component ownership pattern: The component that emits events owns the store
|
|
357
|
-
// Component B (the emitter) declares the shape of events it will emit upfront
|
|
358
|
-
type EventBusSchema = {
|
|
357
|
+
type Events = {
|
|
359
358
|
userLogin: { userId: number; timestamp: number }
|
|
360
359
|
userLogout: { userId: number }
|
|
361
|
-
userUpdate: { userId: number; profile: { name: string } }
|
|
362
360
|
}
|
|
363
361
|
|
|
364
|
-
|
|
365
|
-
const eventBus = createStore<EventBusSchema>({
|
|
362
|
+
const eventBus = createStore<Events>({
|
|
366
363
|
userLogin: UNSET,
|
|
367
|
-
userLogout: UNSET
|
|
368
|
-
userUpdate: UNSET,
|
|
364
|
+
userLogout: UNSET
|
|
369
365
|
})
|
|
370
366
|
|
|
371
|
-
|
|
372
|
-
const emit = <K extends keyof EventBusSchema>(
|
|
373
|
-
event: K,
|
|
374
|
-
data: EventBusSchema[K],
|
|
375
|
-
) => {
|
|
367
|
+
const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
|
|
376
368
|
eventBus[event].set(data)
|
|
377
369
|
}
|
|
378
370
|
|
|
379
|
-
|
|
380
|
-
const on = <K extends keyof EventBusSchema>(
|
|
381
|
-
event: K,
|
|
382
|
-
callback: (data: EventBusSchema[K]) => void,
|
|
383
|
-
) =>
|
|
371
|
+
const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
|
|
384
372
|
createEffect(() => {
|
|
385
373
|
const data = eventBus[event].get()
|
|
386
374
|
if (data !== UNSET) callback(data)
|
|
387
375
|
})
|
|
388
|
-
|
|
389
|
-
// Usage example:
|
|
390
|
-
// Component A listens for user events
|
|
391
|
-
on('userLogin', (data) => {
|
|
392
|
-
console.log('User logged in:', data) // data is properly typed
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
// Component B emits user events
|
|
396
|
-
eventBus.userLogin.set({ userId: 123, timestamp: Date.now() })
|
|
397
376
|
```
|
|
398
377
|
|
|
399
|
-
###
|
|
400
|
-
|
|
378
|
+
### Data Processing Pipelines
|
|
401
379
|
```typescript
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
{ id: 1, value: 10, category: 'A' },
|
|
405
|
-
{ id: 2, value: 20, category: 'B' },
|
|
406
|
-
{ id: 3, value: 15, category: 'A' }
|
|
407
|
-
])
|
|
408
|
-
|
|
409
|
-
// Multi-step transformation pipeline
|
|
410
|
-
const processedData = rawData
|
|
380
|
+
const rawData = new List([{ id: 1, value: 10 }])
|
|
381
|
+
const processed = rawData
|
|
411
382
|
.deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
|
|
412
|
-
.deriveCollection(item => ({ ...item,
|
|
413
|
-
|
|
414
|
-
const categoryTotals = new Collection(processedData, async (item, abort) => {
|
|
415
|
-
// Simulate async processing
|
|
416
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
417
|
-
return { category: item.category, contribution: item.doubled }
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// Aggregation can be done with computed signals
|
|
421
|
-
const totals = new Memo(() => {
|
|
422
|
-
const items = categoryTotals.get()
|
|
423
|
-
return items.reduce((acc, item) => {
|
|
424
|
-
acc[item.category] = (acc[item.category] || 0) + item.contribution
|
|
425
|
-
return acc
|
|
426
|
-
}, {} as Record<string, number>)
|
|
427
|
-
})
|
|
383
|
+
.deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
|
|
428
384
|
```
|
|
429
385
|
|
|
430
|
-
### Stable Keys
|
|
386
|
+
### Stable List Keys
|
|
431
387
|
```typescript
|
|
432
|
-
// Managing a list of items where order matters but identity persists
|
|
433
388
|
const playlist = new List([
|
|
434
|
-
{ id: 'track1', title: 'Song A'
|
|
435
|
-
{ id: 'track2', title: 'Song B', duration: 210 }
|
|
389
|
+
{ id: 'track1', title: 'Song A' }
|
|
436
390
|
], track => track.id)
|
|
437
391
|
|
|
438
|
-
|
|
439
|
-
const firstTrackSignal = playlist.byKey('track1')
|
|
440
|
-
const firstTrack = firstTrackSignal?.get()
|
|
441
|
-
|
|
442
|
-
// Reorder playlist while maintaining references
|
|
392
|
+
const firstTrack = playlist.byKey('track1') // Persists through sorting
|
|
443
393
|
playlist.sort((a, b) => a.title.localeCompare(b.title))
|
|
444
|
-
// firstTrackSignal still points to the same track!
|
|
445
|
-
|
|
446
|
-
// Insert new track at specific position
|
|
447
|
-
playlist.splice(1, 0, { id: 'track3', title: 'Song C', duration: 195 })
|
|
448
|
-
|
|
449
|
-
// Find current position of a track by its stable key
|
|
450
|
-
const track1Position = playlist.indexOfKey('track1')
|
|
451
|
-
const keyAtPosition2 = playlist.keyAt(2)
|
|
452
394
|
```
|
|
453
|
-
|
|
454
|
-
**Component ownership principle**: The component that emits events should own and initialize the event store. This creates clear boundaries and prevents coupling issues.
|
|
455
|
-
|
|
456
|
-
**Why this pattern?**: By having the owning component (Component B) initialize all known events with `UNSET`, we get:
|
|
457
|
-
1. **Fine-grained reactivity**: Each `on()` call establishes a direct dependency on the specific event signal
|
|
458
|
-
2. **Full type safety**: Event names and data shapes are constrained at compile time
|
|
459
|
-
3. **Simple logic**: No conditional updates, no store-wide watching in `on()`
|
|
460
|
-
4. **Clear ownership**: Component B declares its event contract upfront with a schema
|
|
461
|
-
5. **Better performance**: Only the specific event listeners trigger, not all listeners
|
|
462
|
-
6. **Explicit dependencies**: Effects track exactly which events they care about
|
|
463
|
-
7. **IntelliSense support**: IDEs can provide autocomplete for event names and data structures
|
|
464
|
-
|
|
465
|
-
This context should help you understand not just what the code does, but why it's structured this way and how to use it effectively in real-world applications.
|