@zeix/cause-effect 0.15.2 → 0.16.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/CLAUDE.md ADDED
@@ -0,0 +1,319 @@
1
+ # Claude Context for Cause & Effect
2
+
3
+ ## Library Overview and Philosophy
4
+
5
+ Cause & Effect is a reactive state management library that implements the signals pattern for JavaScript and TypeScript applications. The library is designed around the principle of **explicit reactivity** - where dependencies are automatically tracked but relationships remain clear and predictable.
6
+
7
+ ### Core Philosophy
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
12
+ - **Performance Conscious**: Minimal overhead through efficient dependency tracking and batching
13
+
14
+ ## Mental Model for Understanding the System
15
+
16
+ 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
+ - **Effects** are like event handlers that trigger side effects when cells change
21
+
22
+ 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.
23
+
24
+ ## Architectural Deep Dive
25
+
26
+ ### The Watcher System
27
+ The core of reactivity lies in the watcher system (`src/system.ts`):
28
+ - Each signal maintains a `Set<Watcher>` of subscribers
29
+ - When a signal's `.get()` method is called during effect/computed execution, it automatically subscribes the current watcher
30
+ - When a signal changes, it notifies all watchers, which then re-execute their callbacks
31
+ - Batching prevents cascade updates during synchronous operations
32
+
33
+ ### Signal Hierarchy and Type System
34
+
35
+ ```typescript
36
+ // Base Signal interface - all signals implement this
37
+ interface Signal<T extends {}> {
38
+ get(): T
39
+ }
40
+
41
+ // Mutable signals extend this with mutation methods
42
+ interface MutableSignal<T extends {}> extends Signal<T> {
43
+ set(value: T): void
44
+ update(fn: (current: T) => T): void
45
+ }
46
+ ```
47
+
48
+ 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.
49
+
50
+ ### Store Signal Deep Dive
51
+
52
+ Store signals are the most complex part of the system. They transform plain objects into reactive data structures:
53
+
54
+ 1. **Property Proxification**: Each property becomes its own signal
55
+ 2. **Nested Reactivity**: Objects within objects recursively become stores
56
+ 3. **Array Support**: Arrays get special treatment with length tracking and efficient sorting
57
+ 4. **Dynamic Properties**: Runtime addition/removal of properties with proper reactivity
58
+
59
+ Key implementation details:
60
+ - Uses Proxy to intercept property access
61
+ - Maintains internal signal instances for each property
62
+ - Supports change notifications for fine-grained updates
63
+ - Handles edge cases like symbol properties and prototype chain
64
+
65
+ ### Computed Signal Memoization Strategy
66
+
67
+ Computed signals implement smart memoization:
68
+ - **Dependency Tracking**: Automatically tracks which signals are accessed during computation
69
+ - **Stale Detection**: Only recalculates when dependencies actually change
70
+ - **Async Support**: Handles Promise-based computations with automatic cancellation
71
+ - **Error Handling**: Preserves error states and prevents cascade failures
72
+ - **Reducer-like Capabilities**: Access to previous value enables state accumulation and transitions
73
+
74
+ ## Advanced Patterns and Best Practices
75
+
76
+ ### When to Use Each Signal Type
77
+
78
+ **State Signals (`createState`)**:
79
+ - Primitive values (numbers, strings, booleans)
80
+ - Objects that you replace entirely rather than mutating properties
81
+ - Simple toggles and flags
82
+ - Values with straightforward update patterns
83
+
84
+ ```typescript
85
+ const count = createState(0)
86
+ const theme = createState<'light' | 'dark'>('light')
87
+ const user = createState<User>({ id: 1, name: 'John Doe', email: 'john@example.com' }) // Replace entire user object
88
+ ```
89
+
90
+ **Store Signals (`createStore`)**:
91
+ - Objects where individual properties change independently
92
+ - Nested data structures
93
+ - Form state where fields update individually
94
+ - Configuration objects with multiple settings
95
+
96
+ ```typescript
97
+ const form = createStore({
98
+ email: '',
99
+ password: '',
100
+ errors: { email: null, password: null }
101
+ })
102
+ // form.email.set('user@example.com') // Only email subscribers react
103
+ ```
104
+
105
+ **Computed Signals (`createComputed`)**:
106
+ - Expensive calculations that should be memoized
107
+ - Derived data that depends on multiple signals
108
+ - Async operations that need automatic cancellation
109
+ - Cross-cutting concerns that multiple components need
110
+
111
+ ```typescript
112
+ const expensiveCalc = createComputed(() => {
113
+ return heavyComputation(data1.get(), data2.get()) // Memoized
114
+ })
115
+
116
+ const userData = createComputed(async (prev, abort) => {
117
+ const id = userId.get()
118
+ if (!id) return prev // Keep previous data if no ID
119
+ const response = await fetch(`/users/${id}`, { signal: abort })
120
+ return response.json()
121
+ })
122
+
123
+ // Reducer-like pattern for state machines
124
+ const gameState = createComputed((currentState) => {
125
+ const action = playerAction.get()
126
+ switch (currentState) {
127
+ case 'menu':
128
+ return action === 'start' ? 'playing' : 'menu'
129
+ case 'playing':
130
+ return action === 'pause' ? 'paused' : action === 'gameover' ? 'ended' : 'playing'
131
+ case 'paused':
132
+ return action === 'resume' ? 'playing' : action === 'quit' ? 'menu' : 'paused'
133
+ case 'ended':
134
+ return action === 'restart' ? 'playing' : action === 'menu' ? 'menu' : 'ended'
135
+ default:
136
+ return 'menu'
137
+ }
138
+ }, 'menu') // Initial state
139
+
140
+ // Accumulating values over time
141
+ const runningTotal = createComputed((previous) => {
142
+ const newValue = currentValue.get()
143
+ return previous + newValue
144
+ }, 0) // Start with 0
145
+ ```
146
+
147
+ ### Error Handling Strategies
148
+
149
+ The library provides several layers of error handling:
150
+
151
+ 1. **Input Validation**: Custom error classes for invalid operations
152
+ 2. **Async Cancellation**: AbortSignal integration prevents stale async operations
153
+ 3. **Error Propagation**: Computed signals preserve and propagate errors
154
+ 4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
155
+
156
+ ```typescript
157
+ // Robust async data fetching with error handling
158
+ const apiData = createComputed(async (abort) => {
159
+ try {
160
+ const response = await fetch('/api/data', { signal: abort })
161
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
162
+ return response.json()
163
+ } catch (error) {
164
+ if (abort.aborted) return UNSET // Cancelled, not an error
165
+ throw error // Real error
166
+ }
167
+ })
168
+
169
+ // Pattern matching for comprehensive state handling
170
+ createEffect(() => {
171
+ match(resolve({ apiData }), {
172
+ ok: ({ apiData }) => updateUI(apiData),
173
+ nil: () => showLoading(),
174
+ err: errors => showError(errors[0].message)
175
+ })
176
+ })
177
+ ```
178
+
179
+ ### Performance Optimization Techniques
180
+
181
+ 1. **Batching Updates**: Use `batch()` for multiple synchronous updates
182
+ 2. **Selective Dependencies**: Structure computed signals to minimize dependencies
183
+ 3. **Cleanup Management**: Properly dispose of effects to prevent memory leaks
184
+ 4. **Shallow vs Deep Equality**: Use appropriate comparison strategies
185
+
186
+ ```typescript
187
+ // Create a user store
188
+ const user = createStore<{ id: number, name: string, email: string, age?: number }>({
189
+ id: 1,
190
+ name: 'John Doe',
191
+ email: 'john@example.com'
192
+ })
193
+
194
+ // Batch multiple updates to prevent intermediate states
195
+ batch(() => {
196
+ user.name.set('Alice Doe')
197
+ user.age.set(30)
198
+ user.email.set('alice@example.com')
199
+ }) // Only triggers effects once at the end
200
+
201
+ // Optimize computed dependencies
202
+ const userDisplay = createComputed(() => {
203
+ const { name, email } = user.get() // Gets entire object
204
+ return `${name} <${email}>`
205
+ })
206
+
207
+ // Better: more granular dependencies
208
+ const userDisplay = createComputed(() => {
209
+ return `${user.name.get()} <${user.email.get()}>` // Only depends on name/email
210
+ })
211
+ ```
212
+
213
+ ## Integration Patterns
214
+
215
+ ### Framework Integration
216
+ The library is framework-agnostic but integrates well with:
217
+ - **React**: Use effects to trigger re-renders
218
+ - **Vue**: Integrate with Vue's reactivity system
219
+ - **Svelte**: Use effects to update Svelte stores
220
+ - **Vanilla JS**: Direct DOM manipulation in effects
221
+
222
+ ### Testing Strategies
223
+ - **Unit Testing**: Test signal logic in isolation
224
+ - **Integration Testing**: Test effect chains and async operations
225
+ - **Mock Testing**: Mock external dependencies in computed signals
226
+ - **Property Testing**: Use random inputs to verify invariants
227
+
228
+ ### Debugging Techniques
229
+ - Use `resolve()` to inspect multiple signal states at once
230
+ - Add logging effects for debugging reactivity chains
231
+ - Use AbortSignal inspection for async operation debugging
232
+ - Leverage TypeScript for compile-time dependency analysis
233
+
234
+ ## Common Pitfalls and How to Avoid Them
235
+
236
+ 1. **Infinite Loops**: Don't update signals within their own computed callbacks
237
+ 2. **Stale Closures**: Be careful with captured values in async operations
238
+ 3. **Memory Leaks**: Always clean up effects when components unmount
239
+ 4. **Over-reactivity**: Structure data to minimize unnecessary updates
240
+ 5. **Async Race Conditions**: Trust the automatic cancellation, don't fight it
241
+
242
+ ## Advanced Use Cases
243
+
244
+ ### Building Reactive Data Structures
245
+ ```typescript
246
+ // Reactive list with computed properties
247
+ const todos = createStore<Todo[]>([])
248
+ const completedCount = createComputed(() =>
249
+ todos.get().filter(todo => todo.completed).length
250
+ )
251
+ const remainingCount = createComputed(() =>
252
+ todos.get().length - completedCount.get()
253
+ )
254
+ ```
255
+
256
+ ### Reactive State Machines
257
+ ```typescript
258
+ const state = createState<'idle' | 'loading' | 'success' | 'error'>('idle')
259
+ const canRetry = () => state.get() === 'error'
260
+ const isLoading = () => state.get() === 'loading'
261
+ ```
262
+
263
+ ### Cross-Component Communication
264
+ ```typescript
265
+ // Component ownership pattern: The component that emits events owns the store
266
+ // Component B (the emitter) declares the shape of events it will emit upfront
267
+ type EventBusSchema = {
268
+ userLogin: { userId: number; timestamp: number }
269
+ userLogout: { userId: number }
270
+ userUpdate: { userId: number; profile: { name: string } }
271
+ }
272
+
273
+ // Initialize the event bus with proper typing
274
+ const eventBus = createStore<EventBusSchema>({
275
+ userLogin: UNSET,
276
+ userLogout: UNSET,
277
+ userUpdate: UNSET,
278
+ })
279
+
280
+ // Type-safe emit function
281
+ const emit = <K extends keyof EventBusSchema>(
282
+ event: K,
283
+ data: EventBusSchema[K],
284
+ ) => {
285
+ eventBus[event].set(data)
286
+ }
287
+
288
+ // Type-safe on function with proper callback typing
289
+ const on = <K extends keyof EventBusSchema>(
290
+ event: K,
291
+ callback: (data: EventBusSchema[K]) => void,
292
+ ) =>
293
+ createEffect(() => {
294
+ const data = eventBus[event].get()
295
+ if (data !== UNSET) callback(data)
296
+ })
297
+
298
+ // Usage example:
299
+ // Component A listens for user events
300
+ on('userLogin', (data) => {
301
+ console.log('User logged in:', data) // data is properly typed
302
+ })
303
+
304
+ // Component B emits user events
305
+ eventBus.userLogin.set({ userId: 123, timestamp: Date.now() })
306
+ ```
307
+
308
+ **Component ownership principle**: The component that emits events should own and initialize the event store. This creates clear boundaries and prevents coupling issues.
309
+
310
+ **Why this pattern?**: By having the owning component (Component B) initialize all known events with `UNSET`, we get:
311
+ 1. **Fine-grained reactivity**: Each `on()` call establishes a direct dependency on the specific event signal
312
+ 2. **Full type safety**: Event names and data shapes are constrained at compile time
313
+ 3. **Simple logic**: No conditional updates, no store-wide watching in `on()`
314
+ 4. **Clear ownership**: Component B declares its event contract upfront with a schema
315
+ 5. **Better performance**: Only the specific event listeners trigger, not all listeners
316
+ 6. **Explicit dependencies**: Effects track exactly which events they care about
317
+ 7. **IntelliSense support**: IDEs can provide autocomplete for event names and data structures
318
+
319
+ 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.