@zeix/cause-effect 0.16.1 → 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.
Files changed (66) hide show
  1. package/.ai-context.md +85 -21
  2. package/.cursorrules +11 -5
  3. package/.github/copilot-instructions.md +64 -13
  4. package/CLAUDE.md +143 -163
  5. package/LICENSE +1 -1
  6. package/README.md +248 -333
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +21 -21
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +139 -0
  12. package/{src → archive}/state.ts +13 -11
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +938 -509
  17. package/index.js +1 -1
  18. package/index.ts +50 -23
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +282 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +305 -0
  24. package/src/classes/ref.ts +68 -0
  25. package/src/classes/state.ts +98 -0
  26. package/src/classes/store.ts +210 -0
  27. package/src/diff.ts +26 -53
  28. package/src/effect.ts +9 -9
  29. package/src/errors.ts +71 -25
  30. package/src/match.ts +5 -12
  31. package/src/resolve.ts +3 -2
  32. package/src/signal.ts +58 -41
  33. package/src/system.ts +79 -42
  34. package/src/util.ts +16 -34
  35. package/test/batch.test.ts +15 -17
  36. package/test/benchmark.test.ts +4 -4
  37. package/test/collection.test.ts +853 -0
  38. package/test/computed.test.ts +138 -130
  39. package/test/diff.test.ts +2 -2
  40. package/test/effect.test.ts +36 -35
  41. package/test/list.test.ts +754 -0
  42. package/test/match.test.ts +25 -25
  43. package/test/ref.test.ts +227 -0
  44. package/test/resolve.test.ts +17 -19
  45. package/test/signal.test.ts +70 -119
  46. package/test/state.test.ts +44 -44
  47. package/test/store.test.ts +253 -929
  48. package/types/index.d.ts +12 -9
  49. package/types/src/classes/collection.d.ts +46 -0
  50. package/types/src/classes/composite.d.ts +15 -0
  51. package/types/src/classes/computed.d.ts +97 -0
  52. package/types/src/classes/list.d.ts +41 -0
  53. package/types/src/classes/ref.d.ts +39 -0
  54. package/types/src/classes/state.d.ts +52 -0
  55. package/types/src/classes/store.d.ts +51 -0
  56. package/types/src/diff.d.ts +8 -12
  57. package/types/src/errors.d.ts +17 -11
  58. package/types/src/signal.d.ts +27 -14
  59. package/types/src/system.d.ts +41 -20
  60. package/types/src/util.d.ts +6 -4
  61. package/src/store.ts +0 -474
  62. package/types/src/collection.d.ts +0 -26
  63. package/types/src/computed.d.ts +0 -33
  64. package/types/src/scheduler.d.ts +0 -55
  65. package/types/src/state.d.ts +0 -24
  66. package/types/src/store.d.ts +0 -65
package/CLAUDE.md CHANGED
@@ -6,20 +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
- - **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.
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
23
23
 
24
24
  ## Architectural Deep Dive
25
25
 
@@ -43,24 +43,53 @@ interface MutableSignal<T extends {}> extends Signal<T> {
43
43
  set(value: T): void
44
44
  update(fn: (current: T) => T): void
45
45
  }
46
+
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
+ }
46
54
  ```
47
55
 
48
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.
49
57
 
50
- ### Store Signal Deep Dive
58
+ ### Collection Architecture
59
+
60
+ Collections are an interface implemented by different reactive array types:
61
+
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
66
+
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
71
+
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
51
76
 
52
- Store signals are the most complex part of the system. They transform plain objects into reactive data structures:
77
+ ### Store and List Architecture
53
78
 
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
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
58
83
 
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
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
88
+
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
64
93
 
65
94
  ### Computed Signal Memoization Strategy
66
95
 
@@ -69,61 +98,92 @@ Computed signals implement smart memoization:
69
98
  - **Stale Detection**: Only recalculates when dependencies actually change
70
99
  - **Async Support**: Handles Promise-based computations with automatic cancellation
71
100
  - **Error Handling**: Preserves error states and prevents cascade failures
72
- - **Reducer-like Capabilities**: Access to previous value enables state accumulation and transitions
101
+ - **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
73
102
 
74
103
  ## Advanced Patterns and Best Practices
75
104
 
76
105
  ### When to Use Each Signal Type
77
106
 
78
- **State Signals (`createState`)**:
107
+ **State Signals (`State`)**:
79
108
  - Primitive values (numbers, strings, booleans)
80
109
  - Objects that you replace entirely rather than mutating properties
81
110
  - Simple toggles and flags
82
111
  - Values with straightforward update patterns
83
112
 
84
113
  ```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
114
+ const count = new State(0)
115
+ const theme = new State<'light' | 'dark'>('light')
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()
88
127
  ```
89
128
 
90
129
  **Store Signals (`createStore`)**:
91
130
  - Objects where individual properties change independently
92
- - Nested data structures
93
- - Form state where fields update individually
94
- - Configuration objects with multiple settings
95
131
 
96
132
  ```typescript
97
- const form = createStore({
98
- email: '',
99
- password: '',
100
- errors: { email: null, password: null }
133
+ const user = createStore({ name: 'Alice', email: 'alice@example.com' })
134
+ user.name.set('Bob') // Only name subscribers react
135
+ ```
136
+
137
+ **List Signals (`new List`)**:
138
+ ```typescript
139
+ const todoList = new List([
140
+ { id: 'task1', text: 'Learn signals' }
141
+ ], todo => todo.id)
142
+ const firstTodo = todoList.byKey('task1') // Access by stable key
143
+ ```
144
+
145
+ **Collection Signals (`new DerivedCollection`)**:
146
+ ```typescript
147
+ const completedTodos = new DerivedCollection(todoList, todo =>
148
+ todo.completed ? { ...todo, status: 'done' } : null
149
+ )
150
+ const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
151
+ const response = await fetch(`/todos/${todo.id}`, { signal: abort })
152
+ return { ...todo, details: await response.json() }
101
153
  })
102
- // form.email.set('user@example.com') // Only email subscribers react
154
+
155
+ // Chain collections for data pipelines
156
+ const urgentTodoSummaries = todoList
157
+ .deriveCollection(todo => ({ ...todo, urgent: todo.priority > 8 }))
158
+ .deriveCollection(todo => todo.urgent ? `URGENT: ${todo.text}` : todo.text)
159
+
160
+ // Collections maintain stable references through List changes
161
+ const firstTodoDetail = todoDetails.byKey('task1') // Computed signal
162
+ todoList.sort() // Reorders list but collection signals remain stable
103
163
  ```
104
164
 
105
- **Computed Signals (`createComputed`)**:
165
+ **Computed Signals (`Memo` and `Task`)**:
106
166
  - Expensive calculations that should be memoized
107
167
  - Derived data that depends on multiple signals
108
168
  - Async operations that need automatic cancellation
109
169
  - Cross-cutting concerns that multiple components need
110
170
 
111
171
  ```typescript
112
- const expensiveCalc = createComputed(() => {
172
+ const expensiveCalc = new Memo(() => {
113
173
  return heavyComputation(data1.get(), data2.get()) // Memoized
114
174
  })
115
175
 
116
- const userData = createComputed(async (prev, abort) => {
176
+ const userData = new Task(async (prev, abort) => {
117
177
  const id = userId.get()
118
178
  if (!id) return prev // Keep previous data if no ID
119
179
  const response = await fetch(`/users/${id}`, { signal: abort })
120
180
  return response.json()
121
181
  })
122
182
 
123
- // Reducer-like pattern for state machines
124
- const gameState = createComputed((currentState) => {
183
+ // Reducer pattern for state machines
184
+ const gameState = new Memo(prev => {
125
185
  const action = playerAction.get()
126
- switch (currentState) {
186
+ switch (prev) {
127
187
  case 'menu':
128
188
  return action === 'start' ? 'playing' : 'menu'
129
189
  case 'playing':
@@ -138,7 +198,7 @@ const gameState = createComputed((currentState) => {
138
198
  }, 'menu') // Initial state
139
199
 
140
200
  // Accumulating values over time
141
- const runningTotal = createComputed((previous) => {
201
+ const runningTotal = new Memo(prev => {
142
202
  const newValue = currentValue.get()
143
203
  return previous + newValue
144
204
  }, 0) // Start with 0
@@ -154,19 +214,13 @@ The library provides several layers of error handling:
154
214
  4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
155
215
 
156
216
  ```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
- }
217
+ // Error handling with resolve() and match()
218
+ const apiData = new Task(async (prev, abort) => {
219
+ const response = await fetch('/api/data', { signal: abort })
220
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
221
+ return response.json()
167
222
  })
168
223
 
169
- // Pattern matching for comprehensive state handling
170
224
  createEffect(() => {
171
225
  match(resolve({ apiData }), {
172
226
  ok: ({ apiData }) => updateUI(apiData),
@@ -176,144 +230,70 @@ createEffect(() => {
176
230
  })
177
231
  ```
178
232
 
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
233
+ ### Performance Optimization
185
234
 
235
+ **Batching**: Use `batchSignalWrites()` for multiple updates
186
236
  ```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)
237
+ batchSignalWrites(() => {
238
+ user.name.set('Alice')
198
239
  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
- })
240
+ }) // Single effect trigger
211
241
  ```
212
242
 
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
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
+ ```
233
250
 
234
- ## Common Pitfalls and How to Avoid Them
251
+ ## Common Pitfalls
235
252
 
236
253
  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
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
241
257
 
242
- ## Advanced Use Cases
258
+ ## Advanced Patterns
243
259
 
244
- ### Building Reactive Data Structures
260
+ ### Event Bus with Type Safety
245
261
  ```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 = {
262
+ type Events = {
268
263
  userLogin: { userId: number; timestamp: number }
269
264
  userLogout: { userId: number }
270
- userUpdate: { userId: number; profile: { name: string } }
271
265
  }
272
266
 
273
- // Initialize the event bus with proper typing
274
- const eventBus = createStore<EventBusSchema>({
267
+ const eventBus = createStore<Events>({
275
268
  userLogin: UNSET,
276
- userLogout: UNSET,
277
- userUpdate: UNSET,
269
+ userLogout: UNSET
278
270
  })
279
271
 
280
- // Type-safe emit function
281
- const emit = <K extends keyof EventBusSchema>(
282
- event: K,
283
- data: EventBusSchema[K],
284
- ) => {
272
+ const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
285
273
  eventBus[event].set(data)
286
274
  }
287
275
 
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
- ) =>
276
+ const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
293
277
  createEffect(() => {
294
278
  const data = eventBus[event].get()
295
279
  if (data !== UNSET) callback(data)
296
280
  })
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
281
  ```
307
282
 
308
- **Component ownership principle**: The component that emits events should own and initialize the event store. This creates clear boundaries and prevents coupling issues.
283
+ ### Data Processing Pipelines
284
+ ```typescript
285
+ const rawData = new List([{ id: 1, value: 10 }])
286
+ const processed = rawData
287
+ .deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
288
+ .deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
289
+ ```
309
290
 
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
291
+ ### Stable List Keys
292
+ ```typescript
293
+ const playlist = new List([
294
+ { id: 'track1', title: 'Song A' }
295
+ ], track => track.id)
318
296
 
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.
297
+ const firstTrack = playlist.byKey('track1') // Persists through sorting
298
+ playlist.sort((a, b) => a.title.localeCompare(b.title))
299
+ ```
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 - 2025 Zeix AG
3
+ Copyright (c) 2024 - 2026 Zeix AG
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal