@zeix/cause-effect 0.17.0 → 0.17.1

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 CHANGED
@@ -9,10 +9,11 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
9
9
  ### Signal Types
10
10
  - **Signal**: Base interface with `.get()` method for value access
11
11
  - **State**: Mutable signals for primitive values (numbers, strings, booleans)
12
+ - **Ref**: Signal wrappers for external objects that change outside the reactive system (DOM elements, Map, Set, Date, third-party objects)
13
+ - **Computed**: Read-only derived signals with automatic memoization, reducer capabilities and async handling
12
14
  - **Store**: Mutable signals for objects with individually reactive properties
13
15
  - **List**: Mutable signals for arrays with individually reactive items
14
- - **Collection**: Read-only derived arrays with item-level memoization and async support
15
- - **Computed**: Read-only derived signals with automatic memoization, reducer capabilities and asyc handling
16
+ - **Collection**: Interface for reactive array-like collections (implemented by DerivedCollection)
16
17
  - **Effect**: Side effect handlers that react to signal changes
17
18
 
18
19
  ### Key Principles
@@ -29,9 +30,10 @@ cause-effect/
29
30
  ├── src/
30
31
  │ ├── classes/
31
32
  │ │ ├── state.ts # Mutable state signals (State)
33
+ │ │ ├── ref.ts # Signal wrapper for external objects (Ref)
32
34
  │ │ ├── store.ts # Object stores with reactive properties
33
35
  │ │ ├── list.ts # Array stores with stable keys (List)
34
- │ │ ├── collection.ts # Read-only derived arrays (Collection)
36
+ │ │ ├── collection.ts # Collection interface and DerivedCollection
35
37
  │ │ └── computed.ts # Computed/derived signals (Memo and Task)
36
38
  | ├── signal.ts # Base signal types and utilities
37
39
  │ ├── effect.ts # Effect system (createEffect)
@@ -55,6 +57,10 @@ const count = new State(42)
55
57
  const name = new State('Alice')
56
58
  const actions = new State<'increment' | 'decrement' | 'reset'>('reset')
57
59
 
60
+ // Ref signals for external objects
61
+ const elementRef = new Ref(document.getElementById('status'))
62
+ const cacheRef = new Ref(new Map([['key', 'value']]))
63
+
58
64
  // Store signals for objects
59
65
  const user = createStore({ name: 'Alice', age: 30 })
60
66
 
@@ -181,15 +187,19 @@ const firstTodo = todoList.byKey('task1')
181
187
  firstTodo?.completed.set(true) // Mark completed
182
188
 
183
189
  // Collections for read-only derived data
184
- const completedTodos = new Collection(todoList, todo =>
190
+ const completedTodos = new DerivedCollection(todoList, todo =>
185
191
  todo.completed ? { ...todo, status: 'done' } : null
186
192
  ).filter(Boolean) // Remove null values
187
193
 
188
194
  // Async collections for enhanced data
189
- const todoWithDetails = new Collection(todoList, async (todo, abort) => {
195
+ const todoWithDetails = new DerivedCollection(todoList, async (todo, abort) => {
190
196
  const response = await fetch(`/todos/${todo.id}/details`, { signal: abort })
191
197
  return { ...todo, details: await response.json() }
192
198
  })
199
+
200
+ // Element collections for DOM reactivity
201
+ const buttons = createElementCollection(document.body, 'button')
202
+ const inputs = createElementCollection(form, 'input[type="text"]')
193
203
  ```
194
204
 
195
205
  ### Derived State
@@ -265,6 +275,10 @@ const processedItems = items.deriveCollection(item => ({
265
275
  const finalResults = processedItems.deriveCollection(item =>
266
276
  item.processed ? { summary: `${item.name} at ${item.timestamp}` } : null
267
277
  ).filter(Boolean)
278
+
279
+ // Ref signal manual notifications
280
+ elementRef.notify() // Notify when DOM element changes externally
281
+ cacheRef.notify() // Notify when Map/Set changes externally
268
282
  ```
269
283
 
270
284
  ## Build and Development
package/.cursorrules CHANGED
@@ -6,9 +6,11 @@ TypeScript/JavaScript reactive state management library using signals pattern
6
6
  ## Core Concepts
7
7
  - Signals: Base reactive primitives with .get() method
8
8
  - State: new State() for primitives/simple values
9
+ - Ref: new Ref() for external objects (DOM, Map, Set) requiring manual .notify()
9
10
  - Computed: new Memo() for derived/memoized values and new Task() for asynchronous computations
10
11
  - Store: createStore() for objects with reactive properties
11
12
  - List: new List() for arrays with stable keys and reactive items
13
+ - Collection: Interface for reactive collections (DerivedCollection, ElementCollection)
12
14
  - Effects: createEffect() for side effects
13
15
 
14
16
  ## Code Style
@@ -31,9 +33,12 @@ TypeScript/JavaScript reactive state management library using signals pattern
31
33
 
32
34
  ## File Structure
33
35
  - src/signal.ts - Base signal types
34
- - src/state.ts - Mutable state signals
35
- - src/store.ts - Object stores
36
- - src/computed.ts - Computed signals
36
+ - src/classes/state.ts - Mutable state signals
37
+ - src/classes/ref.ts - Signal wrappers for external objects
38
+ - src/classes/store.ts - Object stores
39
+ - src/classes/list.ts - Array stores with stable keys
40
+ - src/classes/collection.ts - Collection interface and DerivedCollection
41
+ - src/classes/computed.ts - Computed signals
37
42
  - src/effect.ts - Effect system
38
43
  - src/system.ts - Core reactivity (watchers, batching)
39
44
  - index.ts - Main exports
@@ -8,18 +8,20 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
8
8
 
9
9
  - **Signals**: Base reactive primitives with `.get()` method
10
10
  - **State**: Mutable signals for primitive values (`new State()`)
11
+ - **Ref**: Signal wrappers for external objects that change outside reactive system (`new Ref()`)
11
12
  - **Computed**: Derived read-only signals with memoization, reducer capabilities and async support (`new Memo()`, `new Task()`)
12
13
  - **Store**: Mutable signals for objects with reactive properties (`createStore()`)
13
14
  - **List**: Mutable signals for arrays with stable keys and reactive entries (`new List()`)
14
- - **Collection**: Read-only derived arrays with item-level memoization and async support (`new Collection()`)
15
+ - **Collection**: Interface for reactive collections (implemented by `DerivedCollection`)
15
16
  - **Effects**: Side effect handlers that react to signal changes (`createEffect()`)
16
17
 
17
18
  ## Key Files Structure
18
19
 
19
20
  - `src/classes/state.ts` - Mutable state signals
21
+ - `src/classes/ref.ts` - Signal wrappers for external objects (DOM, Map, Set, etc.)
20
22
  - `src/classes/store.ts` - Object stores with reactive properties
21
23
  - `src/classes/list.ts` - Array stores with stable keys and reactive items
22
- - `src/classes/collection.ts` - Read-only derived arrays with memoization
24
+ - `src/classes/collection.ts` - Collection interface and DerivedCollection implementation
23
25
  - `src/classes/computed.ts` - Computed/derived signals
24
26
  - `src/signal.ts` - Base signal types and utilities
25
27
  - `src/effect.ts` - Effect system
@@ -77,6 +79,10 @@ const count = new State(42)
77
79
  const name = new State('Alice')
78
80
  const actions = new State<'increment' | 'decrement'>('increment')
79
81
 
82
+ // Ref for external objects
83
+ const elementRef = new Ref(document.getElementById('status'))
84
+ const cacheRef = new Ref(new Map())
85
+
80
86
  // Store for objects
81
87
  const user = createStore({ name: 'Alice', age: 30 })
82
88
 
@@ -145,7 +151,6 @@ function isSignal<T extends {}>(value: unknown): value is Signal<T>
145
151
  const items = new List(['a', 'b', 'c'])
146
152
 
147
153
  // String prefix keys
148
- // Lists with stable keys
149
154
  const items = new List(['apple', 'banana'], 'fruit')
150
155
  // Creates keys: 'fruit0', 'fruit1'
151
156
 
@@ -156,7 +161,7 @@ const users = new List([
156
161
  ], user => user.id) // Uses user.id as stable key
157
162
 
158
163
  // Collections derived from lists
159
- const userProfiles = new Collection(users, user => ({
164
+ const userProfiles = new DerivedCollection(users, user => ({
160
165
  ...user,
161
166
  displayName: `${user.name} (ID: ${user.id})`
162
167
  }))
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
67
-
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
58
+ ### Collection Architecture
75
59
 
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
60
+ Collections are an interface implemented by different reactive array types:
81
61
 
82
- ### Store Signal Deep Dive
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
83
66
 
84
- Store signals are the most complex part of the system. They transform plain objects or arrays into reactive data structures:
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
85
71
 
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
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
90
76
 
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
77
+ ### Store and List Architecture
98
78
 
99
- ### MutableComposite Architecture
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
100
83
 
101
- Both Store and List signals are built on top of `MutableComposite`, which provides the common reactive property management:
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
102
88
 
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
+ - Event system for granular add/change/remove notifications
92
+ - Lazy signal creation and automatic cleanup
114
93
 
115
94
  ### Computed Signal Memoization Strategy
116
95
 
@@ -134,51 +113,41 @@ Computed signals implement smart memoization:
134
113
  ```typescript
135
114
  const count = new State(0)
136
115
  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
116
+ ```
117
+
118
+ **Ref Signals (`new Ref`)**:
119
+ - External objects that change outside the reactive system
120
+ - DOM elements, Map, Set, Date objects, third-party APIs
121
+ - Requires manual `.notify()` when external object changes
122
+
123
+ ```typescript
124
+ const elementRef = new Ref(document.getElementById('status'))
125
+ const cacheRef = new Ref(new Map())
126
+ // When external change occurs: cacheRef.notify()
138
127
  ```
139
128
 
140
129
  **Store Signals (`createStore`)**:
141
130
  - Objects where individual properties change independently
142
- - Nested data structures
143
- - Form state where fields update individually
144
- - Configuration objects with multiple settings
145
131
 
146
132
  ```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
133
+ const user = createStore({ name: 'Alice', email: 'alice@example.com' })
134
+ user.name.set('Bob') // Only name subscribers react
153
135
  ```
154
136
 
155
137
  **List Signals (`new List`)**:
156
-
157
- ```ts
158
- // Lists with stable keys
138
+ ```typescript
159
139
  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))
140
+ { id: 'task1', text: 'Learn signals' }
141
+ ], todo => todo.id)
142
+ const firstTodo = todoList.byKey('task1') // Access by stable key
170
143
  ```
171
144
 
172
- **Collection Signals (`new Collection`)**:
173
-
174
- ```ts
175
- // Read-only derived arrays with memoization
176
- const completedTodos = new Collection(todoList, todo =>
145
+ **Collection Signals (`new DerivedCollection`)**:
146
+ ```typescript
147
+ const completedTodos = new DerivedCollection(todoList, todo =>
177
148
  todo.completed ? { ...todo, status: 'done' } : null
178
149
  )
179
-
180
- // Async transformations with cancellation
181
- const todoDetails = new Collection(todoList, async (todo, abort) => {
150
+ const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
182
151
  const response = await fetch(`/todos/${todo.id}`, { signal: abort })
183
152
  return { ...todo, details: await response.json() }
184
153
  })
@@ -245,19 +214,13 @@ The library provides several layers of error handling:
245
214
  4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
246
215
 
247
216
  ```typescript
248
- // Robust async data fetching with error handling
217
+ // Error handling with resolve() and match()
249
218
  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
- }
219
+ const response = await fetch('/api/data', { signal: abort })
220
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
221
+ return response.json()
258
222
  })
259
223
 
260
- // Pattern matching for comprehensive state handling
261
224
  createEffect(() => {
262
225
  match(resolve({ apiData }), {
263
226
  ok: ({ apiData }) => updateUI(apiData),
@@ -267,199 +230,70 @@ createEffect(() => {
267
230
  })
268
231
  ```
269
232
 
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
233
+ ### Performance Optimization
276
234
 
235
+ **Batching**: Use `batchSignalWrites()` for multiple updates
277
236
  ```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
237
  batchSignalWrites(() => {
287
- user.name.set('Alice Doe')
288
- user.age.set(30)
238
+ user.name.set('Alice')
289
239
  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
- })
240
+ }) // Single effect trigger
302
241
  ```
303
242
 
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
243
+ **Granular Dependencies**: Structure computed signals to minimize dependencies
244
+ ```typescript
245
+ // Bad: depends on entire user object
246
+ const display = new Memo(() => user.get().name + user.get().email)
247
+ // Good: only depends on specific properties
248
+ const display = new Memo(() => user.name.get() + user.email.get())
249
+ ```
324
250
 
325
- ## Common Pitfalls and How to Avoid Them
251
+ ## Common Pitfalls
326
252
 
327
253
  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
254
+ 2. **Memory Leaks**: Clean up effects when components unmount
255
+ 3. **Over-reactivity**: Structure data to minimize unnecessary updates
256
+ 4. **Async Race Conditions**: Trust automatic cancellation with AbortSignal
332
257
 
333
- ## Advanced Use Cases
258
+ ## Advanced Patterns
334
259
 
335
- ### Building Reactive Data Structures
260
+ ### Event Bus with Type Safety
336
261
  ```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 = {
262
+ type Events = {
359
263
  userLogin: { userId: number; timestamp: number }
360
264
  userLogout: { userId: number }
361
- userUpdate: { userId: number; profile: { name: string } }
362
265
  }
363
266
 
364
- // Initialize the event bus with proper typing
365
- const eventBus = createStore<EventBusSchema>({
267
+ const eventBus = createStore<Events>({
366
268
  userLogin: UNSET,
367
- userLogout: UNSET,
368
- userUpdate: UNSET,
269
+ userLogout: UNSET
369
270
  })
370
271
 
371
- // Type-safe emit function
372
- const emit = <K extends keyof EventBusSchema>(
373
- event: K,
374
- data: EventBusSchema[K],
375
- ) => {
272
+ const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
376
273
  eventBus[event].set(data)
377
274
  }
378
275
 
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
- ) =>
276
+ const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
384
277
  createEffect(() => {
385
278
  const data = eventBus[event].get()
386
279
  if (data !== UNSET) callback(data)
387
280
  })
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
281
  ```
398
282
 
399
- ### Reactive Data Processing Pipelines
400
-
283
+ ### Data Processing Pipelines
401
284
  ```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
285
+ const rawData = new List([{ id: 1, value: 10 }])
286
+ const processed = rawData
411
287
  .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
- })
288
+ .deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
428
289
  ```
429
290
 
430
- ### Stable Keys for Persistent Item Identity
291
+ ### Stable List Keys
431
292
  ```typescript
432
- // Managing a list of items where order matters but identity persists
433
293
  const playlist = new List([
434
- { id: 'track1', title: 'Song A', duration: 180 },
435
- { id: 'track2', title: 'Song B', duration: 210 }
294
+ { id: 'track1', title: 'Song A' }
436
295
  ], track => track.id)
437
296
 
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
297
+ const firstTrack = playlist.byKey('track1') // Persists through sorting
443
298
  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
299
  ```
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.