@zeix/cause-effect 0.15.2 → 0.16.0
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 +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +136 -166
- package/eslint.config.js +1 -1
- package/index.dev.js +125 -129
- package/index.js +1 -1
- package/index.ts +22 -22
- package/package.json +1 -1
- package/src/computed.ts +40 -29
- package/src/effect.ts +15 -12
- package/src/errors.ts +8 -0
- package/src/signal.ts +6 -6
- package/src/state.ts +27 -20
- package/src/store.ts +99 -121
- package/src/system.ts +122 -0
- package/src/util.ts +1 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +507 -71
- package/test/effect.test.ts +60 -60
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +7 -7
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +476 -183
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +8 -8
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +4 -1
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +27 -41
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +1 -2
- package/src/scheduler.ts +0 -172
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.
|