@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.
Files changed (50) hide show
  1. package/.ai-context.md +26 -5
  2. package/.cursorrules +8 -3
  3. package/.github/copilot-instructions.md +13 -4
  4. package/CLAUDE.md +191 -262
  5. package/README.md +268 -420
  6. package/archive/collection.ts +23 -25
  7. package/archive/computed.ts +5 -4
  8. package/archive/list.ts +21 -28
  9. package/archive/memo.ts +4 -2
  10. package/archive/state.ts +2 -1
  11. package/archive/store.ts +21 -32
  12. package/archive/task.ts +6 -9
  13. package/index.dev.js +411 -220
  14. package/index.js +1 -1
  15. package/index.ts +25 -8
  16. package/package.json +1 -1
  17. package/src/classes/collection.ts +103 -77
  18. package/src/classes/composite.ts +28 -33
  19. package/src/classes/computed.ts +90 -31
  20. package/src/classes/list.ts +39 -33
  21. package/src/classes/ref.ts +96 -0
  22. package/src/classes/state.ts +41 -8
  23. package/src/classes/store.ts +47 -30
  24. package/src/diff.ts +2 -1
  25. package/src/effect.ts +19 -9
  26. package/src/errors.ts +31 -1
  27. package/src/match.ts +5 -12
  28. package/src/resolve.ts +3 -2
  29. package/src/signal.ts +0 -1
  30. package/src/system.ts +159 -43
  31. package/src/util.ts +0 -10
  32. package/test/collection.test.ts +383 -67
  33. package/test/computed.test.ts +268 -11
  34. package/test/effect.test.ts +2 -2
  35. package/test/list.test.ts +249 -21
  36. package/test/ref.test.ts +381 -0
  37. package/test/state.test.ts +13 -13
  38. package/test/store.test.ts +473 -28
  39. package/types/index.d.ts +6 -5
  40. package/types/src/classes/collection.d.ts +27 -12
  41. package/types/src/classes/composite.d.ts +4 -4
  42. package/types/src/classes/computed.d.ts +17 -0
  43. package/types/src/classes/list.d.ts +6 -6
  44. package/types/src/classes/ref.d.ts +48 -0
  45. package/types/src/classes/state.d.ts +9 -0
  46. package/types/src/classes/store.d.ts +4 -4
  47. package/types/src/effect.d.ts +1 -2
  48. package/types/src/errors.d.ts +9 -1
  49. package/types/src/system.d.ts +40 -24
  50. 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**: No hidden subscriptions or magic - dependencies are tracked through `.get()` calls
10
- - **Functional Design**: Immutable by default, with pure functions and predictable state updates
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 like input cells where you can directly enter values
18
- - **Computed signals** are like formula cells that automatically recalculate when their dependencies change
19
- - **Store signals** are like structured data tables where individual columns (properties) are reactive
20
- - **List signals** with stable keys are like tables with persistent row IDs that survive sorting and reordering
21
- - **Effects** are like event handlers that trigger side effects when cells change
22
-
23
- The key insight is that the system tracks which cells (signals) are read during computation, creating an automatic dependency graph that ensures minimal and correct updates.
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 signals are read-only derived arrays
49
- interface Collection<T extends {}, U extends {}> extends Signal<T[]> {
50
- at(index: number): Computed<T> | undefined
51
- byKey(key: string): Computed<T> | undefined
52
- deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R, T>
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 Signal Deep Dive
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
- Key implementation details:
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
- Collection Architecture:
77
- - **BaseCollection**: Core implementation with signal management and event handling
78
- - **Computed Creation**: Each source item gets its own computed signal with transformation callback
79
- - **Source Synchronization**: Listens to source events and maintains parallel structure
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
- ### Store Signal Deep Dive
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
- Store signals are the most complex part of the system. They transform plain objects or arrays into reactive data structures:
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
- 1. **Property Proxification**: Each property becomes its own signal
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
- Key implementation details:
92
- - Uses Proxy to intercept property access
93
- - Maintains internal signal instances for each property
94
- - Supports change notifications for fine-grained updates
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
- ### MutableComposite Architecture
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
- Both Store and List signals are built on top of `MutableComposite`, which provides the common reactive property management:
102
-
103
- 1. **Signal Management**: Maintains a `Map<string, MutableSignal>` of property signals
104
- 2. **Change Detection**: Uses `diff()` to detect actual changes before triggering updates
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
- const user = new State<User>({ id: 1, name: 'John Doe', email: 'john@example.com' }) // Replace entire user object
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 form = createStore({
148
- email: '',
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
- { id: 'task2', text: 'Build app' }
162
- ], todo => todo.id) // Use todo.id as stable key
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 Collection`)**:
173
-
174
- ```ts
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
- // Robust async data fetching with error handling
312
+ // Error handling with resolve() and match()
249
313
  const apiData = new Task(async (prev, abort) => {
250
- try {
251
- const response = await fetch('/api/data', { signal: abort })
252
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
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 Techniques
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 Doe')
288
- user.age.set(30)
333
+ user.name.set('Alice')
289
334
  user.email.set('alice@example.com')
290
- }) // Only triggers effects once at the end
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
- ## Integration Patterns
305
-
306
- ### Framework Integration
307
- The library is framework-agnostic but integrates well with:
308
- - **React**: Use effects to trigger re-renders
309
- - **Vue**: Integrate with Vue's reactivity system
310
- - **Svelte**: Use effects to update Svelte stores
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 and How to Avoid Them
346
+ ## Common Pitfalls
326
347
 
327
348
  1. **Infinite Loops**: Don't update signals within their own computed callbacks
328
- 2. **Stale Closures**: Be careful with captured values in async operations
329
- 3. **Memory Leaks**: Always clean up effects when components unmount
330
- 4. **Over-reactivity**: Structure data to minimize unnecessary updates
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 Use Cases
353
+ ## Advanced Patterns
334
354
 
335
- ### Building Reactive Data Structures
355
+ ### Event Bus with Type Safety
336
356
  ```typescript
337
- // Reactive list with computed properties
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
- // Initialize the event bus with proper typing
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
- // Type-safe emit function
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
- // Type-safe on function with proper callback typing
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
- ### Reactive Data Processing Pipelines
400
-
378
+ ### Data Processing Pipelines
401
379
  ```typescript
402
- // Build complex data processing with Collections
403
- const rawData = new List([
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, category: item.category.toLowerCase() }))
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 for Persistent Item Identity
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', duration: 180 },
435
- { id: 'track2', title: 'Song B', duration: 210 }
389
+ { id: 'track1', title: 'Song A' }
436
390
  ], track => track.id)
437
391
 
438
- // Get persistent reference to a specific track
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.