@zeix/cause-effect 0.17.2 → 0.18.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.
Files changed (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -81
package/CLAUDE.md CHANGED
@@ -14,21 +14,34 @@ Cause & Effect is a reactive state management library that implements the signal
14
14
 
15
15
  Think of signals as **observable cells** in a spreadsheet:
16
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
17
+ - **Sensor signals** are read-only cells that track external input (mouse position, resize, mutable objects) and update lazily
18
+ - **Memo signals** are formula cells that automatically recalculate when dependencies change
19
+ - **Task signals** are async formula cells with abort semantics and pending state
19
20
  - **Store signals** are structured tables where individual columns (properties) are reactive
20
21
  - **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
+ - **Collection signals** are externally-fed reactive tables with a watched lifecycle (WebSocket, SSE), or derived tables from Lists/Collections via `.deriveCollection()`
22
23
  - **Effects** are event handlers that trigger side effects when cells change
23
24
 
24
25
  ## Architectural Deep Dive
25
26
 
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
27
+ ### The Graph System
28
+ The core of reactivity lies in the linked graph system (`src/graph.ts`):
29
+ - Each source node maintains a linked list of `Edge` entries pointing to sink nodes
30
+ - When a signal's `.get()` method is called during effect/memo/task execution, it automatically links the source to the active sink via `link()`
31
+ - When a signal changes, it calls `propagate()` on all linked sinks, which flags them dirty
32
+ - `flush()` processes queued effects after propagation
33
+ - `batch()` defers flushing until the outermost batch completes
34
+ - `trimSources()` removes stale edges after recomputation
35
+
36
+ ### Node Types in the Graph
37
+
38
+ ```
39
+ StateNode<T> — source-only with equality + guard (State, Sensor)
40
+ MemoNode<T> — source + sink (Memo, Store, List, Collection)
41
+ TaskNode<T> — source + sink + async (Task)
42
+ EffectNode — sink + owner (Effect)
43
+ Scope — owner-only (createScope)
44
+ ```
32
45
 
33
46
  ### Signal Hierarchy and Type System
34
47
 
@@ -38,18 +51,33 @@ interface Signal<T extends {}> {
38
51
  get(): T
39
52
  }
40
53
 
41
- // Mutable signals extend this with mutation methods
42
- interface MutableSignal<T extends {}> extends Signal<T> {
54
+ // State adds mutation methods
55
+ type State<T extends {}> = {
56
+ get(): T
43
57
  set(value: T): void
44
58
  update(fn: (current: T) => T): void
45
59
  }
46
60
 
47
- // Collection interface - implemented by various collection types
48
- interface Collection<T extends {}> {
61
+ // Memo is read-only computed
62
+ type Memo<T extends {}> = {
63
+ get(): T
64
+ }
65
+
66
+ // Task adds async control
67
+ type Task<T extends {}> = {
68
+ get(): T
69
+ isPending(): boolean
70
+ abort(): void
71
+ }
72
+
73
+ // Collection interface
74
+ type Collection<T extends {}> = {
49
75
  get(): T[]
50
76
  at(index: number): Signal<T> | undefined
51
77
  byKey(key: string): Signal<T> | undefined
52
- deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R>
78
+ keys(): IterableIterator<string>
79
+ deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R>
80
+ deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>
53
81
  }
54
82
  ```
55
83
 
@@ -57,141 +85,88 @@ The generic constraint `T extends {}` is crucial - it excludes `null` and `undef
57
85
 
58
86
  ### Collection Architecture
59
87
 
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
88
+ **Collections** (`createCollection`): Externally-driven collections with watched lifecycle
89
+ - Created via `createCollection(start, options?)` — mirrors `createSensor(start, options?)`
90
+ - The `start` callback receives an `applyChanges(diffResult)` function for granular add/change/remove operations
91
+ - `options.value` provides initial items (default `[]`), `options.keyConfig` configures key generation
92
+ - Lazy activation: `start` callback invoked on first effect access, cleanup when unwatched
93
+
94
+ **Derived Collections** (`deriveCollection`): Transformed from Lists or other Collections
95
+ - Created via `list.deriveCollection(callback)` or `collection.deriveCollection(callback)`
96
+ - Also available as `deriveCollection(source, callback)` (internal helper, exported for advanced use)
97
+ - Item-level memoization: sync callbacks use `createMemo`, async callbacks use `createTask`
98
+ - Structural changes tracked via an internal `MemoNode` that reads `source.keys()`
65
99
  - Chainable for data pipelines
66
100
 
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
76
-
77
101
  ### Store and List Architecture
78
102
 
79
103
  **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
83
-
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
104
+ - Each property becomes its own State signal (primitives) or nested Store (objects) via Proxy
105
+ - Uses an internal `MemoNode` for structural reactivity (add/remove properties)
106
+ - `diff()` computes granular changes when calling `set()`
107
+ - Dynamic property addition/removal with `add()`/`remove()`
108
+
109
+ **List signals** (`createList`): Arrays with stable keys and reactive items
110
+ - Each item becomes a `State` signal in a `Map<string, State<T>>`
111
+ - Uses an internal `MemoNode` for structural reactivity
112
+ - Configurable key generation: auto-increment, string prefix, or function
87
113
  - 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
- - Hook system for granular add/change/remove notifications
92
- - Lazy signal creation and automatic cleanup
114
+ - `deriveCollection()` creates derived Collections
93
115
 
94
116
  ### Computed Signal Memoization Strategy
95
117
 
96
118
  Computed signals implement smart memoization:
97
- - **Dependency Tracking**: Automatically tracks which signals are accessed during computation
98
- - **Stale Detection**: Only recalculates when dependencies actually change
99
- - **Async Support**: Handles Promise-based computations with automatic cancellation
119
+ - **Dependency Tracking**: Automatically tracks which signals are accessed during computation via `link()`
120
+ - **Stale Detection**: Flag-based dirty checking (CLEAN, CHECK, DIRTY) — only recalculates when dependencies actually change
121
+ - **Async Support**: `createTask` handles Promise-based computations with automatic AbortController cancellation
100
122
  - **Error Handling**: Preserves error states and prevents cascade failures
101
123
  - **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
102
124
 
103
- ## Resource Management with Watch Hooks
125
+ ## Resource Management with Watch Callbacks
104
126
 
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:
127
+ Sensor and Collection signals use a **start callback** pattern 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
128
 
107
129
  ```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
- }
130
+ // Sensor: track external input with state updates
131
+ const mousePos = createSensor<{ x: number; y: number }>((set) => {
132
+ const handler = (e: MouseEvent) => set({ x: e.clientX, y: e.clientY })
133
+ window.addEventListener('mousemove', handler)
134
+ return () => window.removeEventListener('mousemove', handler)
119
135
  })
120
136
 
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
- })
137
+ // Sensor: observe a mutable external object (SKIP_EQUALITY)
138
+ const element = createSensor<HTMLElement>((set) => {
139
+ const node = document.getElementById('status')!
140
+ set(node)
141
+ const observer = new MutationObserver(() => set(node))
142
+ observer.observe(node, { attributes: true })
143
+ return () => observer.disconnect() // cleanup when unwatched
144
+ }, { value: node, equals: SKIP_EQUALITY })
145
+
146
+ // Collection: receive keyed data from external source
147
+ const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
148
+ const es = new EventSource('/feed')
149
+ es.onmessage = (e) => applyChanges(JSON.parse(e.data))
150
+ return () => es.close()
151
+ }, { keyConfig: item => item.id })
144
152
  ```
145
153
 
146
- **List Watch Hooks**: Two-tier system for collection and item resources
154
+ Store and List signals support an optional `watched` callback in their options:
147
155
 
148
156
  ```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
157
+ const user = createStore({ name: 'Alice', email: 'alice@example.com' }, {
158
+ watched: () => {
159
+ console.log('Store is now being watched')
160
+ const ws = new WebSocket('/updates')
161
+ return () => ws.close() // cleanup returned as Cleanup
162
+ }
180
163
  })
181
164
  ```
182
165
 
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
166
+ **Watch Lifecycle**:
167
+ 1. First effect accesses signal start/watched callback executed
168
+ 2. Last effect stops watching → returned cleanup function executed
169
+ 3. New effect accesses signal start/watched callback executed again
195
170
 
196
171
  This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
197
172
 
@@ -199,126 +174,144 @@ This pattern enables **lazy resource allocation** - resources are only consumed
199
174
 
200
175
  ### When to Use Each Signal Type
201
176
 
202
- **State Signals (`State`)**:
177
+ **State (`createState`)**:
203
178
  - Primitive values (numbers, strings, booleans)
204
179
  - Objects that you replace entirely rather than mutating properties
205
180
  - Simple toggles and flags
206
- - Values with straightforward update patterns
207
181
 
208
182
  ```typescript
209
- const count = new State(0)
210
- const theme = new State<'light' | 'dark'>('light')
183
+ const count = createState(0)
184
+ const theme = createState<'light' | 'dark'>('light')
211
185
  ```
212
186
 
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
187
+ **Sensor (`createSensor`)**:
188
+ - External input streams (mouse position, window size, media queries)
189
+ - External mutable objects observed via MutationObserver, IntersectionObserver, etc.
190
+ - Returns a read-only signal that activates lazily and tears down when unwatched
191
+ - Starts undefined until first `set()` unless `options.value` is provided
217
192
 
218
193
  ```typescript
219
- const elementRef = new Ref(document.getElementById('status'))
220
- const cacheRef = new Ref(new Map())
221
- // When external change occurs: cacheRef.notify()
194
+ // Tracking external values (default equality)
195
+ const windowSize = createSensor<{ w: number; h: number }>((set) => {
196
+ const update = () => set({ w: innerWidth, h: innerHeight })
197
+ update()
198
+ window.addEventListener('resize', update)
199
+ return () => window.removeEventListener('resize', update)
200
+ })
201
+
202
+ // Observing a mutable object (SKIP_EQUALITY)
203
+ const el = createSensor<HTMLElement>((set) => {
204
+ const node = document.getElementById('box')!
205
+ set(node)
206
+ const obs = new MutationObserver(() => set(node))
207
+ obs.observe(node, { attributes: true })
208
+ return () => obs.disconnect()
209
+ }, { value: node, equals: SKIP_EQUALITY })
222
210
  ```
223
211
 
224
- **Store Signals (`createStore`)**:
212
+ **Store (`createStore`)**:
225
213
  - Objects where individual properties change independently
214
+ - Proxy-based: access properties directly as signals
226
215
 
227
216
  ```typescript
228
217
  const user = createStore({ name: 'Alice', email: 'alice@example.com' })
229
218
  user.name.set('Bob') // Only name subscribers react
230
219
  ```
231
220
 
232
- **List Signals (`new List`)**:
221
+ **List (`createList`)**:
222
+ - Arrays with stable keys and reactive items
223
+
233
224
  ```typescript
234
- const todoList = new List([
225
+ const todoList = createList([
235
226
  { id: 'task1', text: 'Learn signals' }
236
- ], todo => todo.id)
227
+ ], { keyConfig: todo => todo.id })
237
228
  const firstTodo = todoList.byKey('task1') // Access by stable key
238
229
  ```
239
230
 
240
- **Collection Signals (`new DerivedCollection`)**:
231
+ **Collection (`createCollection`)**:
232
+ - Externally-driven keyed collections (WebSocket streams, SSE, external data feeds)
233
+ - Mirrors `createSensor(start, options?)` — start callback pattern with watched lifecycle
234
+ - Same `Collection` interface — `.get()`, `.byKey()`, `.keys()`, `.deriveCollection()`
235
+
236
+ ```typescript
237
+ const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
238
+ const ws = new WebSocket('/feed')
239
+ ws.onmessage = (e) => applyChanges(JSON.parse(e.data))
240
+ return () => ws.close()
241
+ }, { keyConfig: item => item.id })
242
+ ```
243
+
244
+ **Derived Collection** (`.deriveCollection()`):
245
+ - Read-only derived transformations of Lists or other Collections
246
+ - Created via `.deriveCollection()` method on List or Collection
247
+
241
248
  ```typescript
242
- const completedTodos = new DerivedCollection(todoList, todo =>
243
- todo.completed ? { ...todo, status: 'done' } : null
244
- )
245
- const todoDetails = new DerivedCollection(todoList, async (todo, abort) => {
246
- const response = await fetch(`/todos/${todo.id}`, { signal: abort })
247
- return { ...todo, details: await response.json() }
249
+ const doubled = numbers.deriveCollection((value: number) => value * 2)
250
+
251
+ // Async transformation
252
+ const enriched = users.deriveCollection(async (user, abort) => {
253
+ const response = await fetch(`/api/${user.id}`, { signal: abort })
254
+ return { ...user, details: await response.json() }
248
255
  })
249
256
 
250
257
  // Chain collections for data pipelines
251
- const urgentTodoSummaries = todoList
258
+ const processed = todoList
252
259
  .deriveCollection(todo => ({ ...todo, urgent: todo.priority > 8 }))
253
260
  .deriveCollection(todo => todo.urgent ? `URGENT: ${todo.text}` : todo.text)
254
-
255
- // Collections maintain stable references through List changes
256
- const firstTodoDetail = todoDetails.byKey('task1') // Computed signal
257
- todoList.sort() // Reorders list but collection signals remain stable
258
261
  ```
259
262
 
260
- **Computed Signals (`Memo` and `Task`)**:
261
- - Expensive calculations that should be memoized
262
- - Derived data that depends on multiple signals
263
- - Async operations that need automatic cancellation
264
- - Cross-cutting concerns that multiple components need
263
+ **Memo (`createMemo`)**:
264
+ - Synchronous derived computations with memoization
265
+ - Reducer pattern with previous value access
265
266
 
266
267
  ```typescript
267
- const expensiveCalc = new Memo(() => {
268
- return heavyComputation(data1.get(), data2.get()) // Memoized
269
- })
270
-
271
- const userData = new Task(async (prev, abort) => {
272
- const id = userId.get()
273
- if (!id) return prev // Keep previous data if no ID
274
- const response = await fetch(`/users/${id}`, { signal: abort })
275
- return response.json()
276
- })
268
+ const doubled = createMemo(() => count.get() * 2)
277
269
 
278
270
  // Reducer pattern for state machines
279
- const gameState = new Memo(prev => {
271
+ const gameState = createMemo(prev => {
280
272
  const action = playerAction.get()
281
273
  switch (prev) {
282
- case 'menu':
283
- return action === 'start' ? 'playing' : 'menu'
284
- case 'playing':
285
- return action === 'pause' ? 'paused' : action === 'gameover' ? 'ended' : 'playing'
286
- case 'paused':
287
- return action === 'resume' ? 'playing' : action === 'quit' ? 'menu' : 'paused'
288
- case 'ended':
289
- return action === 'restart' ? 'playing' : action === 'menu' ? 'menu' : 'ended'
290
- default:
291
- return 'menu'
274
+ case 'menu': return action === 'start' ? 'playing' : 'menu'
275
+ case 'playing': return action === 'pause' ? 'paused' : 'playing'
276
+ default: return prev
292
277
  }
293
- }, 'menu') // Initial state
278
+ }, { value: 'menu' })
294
279
 
295
- // Accumulating values over time
296
- const runningTotal = new Memo(prev => {
297
- const newValue = currentValue.get()
298
- return previous + newValue
299
- }, 0) // Start with 0
280
+ // Accumulating values
281
+ const runningTotal = createMemo(prev => prev + currentValue.get(), { value: 0 })
282
+ ```
283
+
284
+ **Task (`createTask`)**:
285
+ - Async computations with automatic cancellation
286
+
287
+ ```typescript
288
+ const userData = createTask(async (prev, abort) => {
289
+ const id = userId.get()
290
+ if (!id) return prev
291
+ const response = await fetch(`/users/${id}`, { signal: abort })
292
+ return response.json()
293
+ })
300
294
  ```
301
295
 
302
296
  ### Error Handling Strategies
303
297
 
304
298
  The library provides several layers of error handling:
305
299
 
306
- 1. **Input Validation**: Custom error classes for invalid operations
300
+ 1. **Input Validation**: `validateSignalValue()` and `validateCallback()` with custom error classes
307
301
  2. **Async Cancellation**: AbortSignal integration prevents stale async operations
308
- 3. **Error Propagation**: Computed signals preserve and propagate errors
309
- 4. **Helper Functions**: `resolve()` and `match()` for ergonomic error handling
302
+ 3. **Error Propagation**: Memo and Task preserve error states and throw on `.get()`
303
+ 4. **Match Helper**: `match()` for ergonomic signal value extraction inside effects
310
304
 
311
305
  ```typescript
312
- // Error handling with resolve() and match()
313
- const apiData = new Task(async (prev, abort) => {
306
+ const apiData = createTask(async (prev, abort) => {
314
307
  const response = await fetch('/api/data', { signal: abort })
315
308
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
316
309
  return response.json()
317
310
  })
318
311
 
319
312
  createEffect(() => {
320
- match(resolve({ apiData }), {
321
- ok: ({ apiData }) => updateUI(apiData),
313
+ match([apiData], {
314
+ ok: ([data]) => updateUI(data),
322
315
  nil: () => showLoading(),
323
316
  err: errors => showError(errors[0].message)
324
317
  })
@@ -327,9 +320,9 @@ createEffect(() => {
327
320
 
328
321
  ### Performance Optimization
329
322
 
330
- **Batching**: Use `batchSignalWrites()` for multiple updates
323
+ **Batching**: Use `batch()` for multiple updates
331
324
  ```typescript
332
- batchSignalWrites(() => {
325
+ batch(() => {
333
326
  user.name.set('Alice')
334
327
  user.email.set('alice@example.com')
335
328
  }) // Single effect trigger
@@ -338,17 +331,19 @@ batchSignalWrites(() => {
338
331
  **Granular Dependencies**: Structure computed signals to minimize dependencies
339
332
  ```typescript
340
333
  // 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())
334
+ const display = createMemo(() => user.get().name + user.get().email)
335
+ // Good: only depends on specific properties
336
+ const display = createMemo(() => user.name.get() + user.email.get())
344
337
  ```
345
338
 
346
339
  ## Common Pitfalls
347
340
 
348
341
  1. **Infinite Loops**: Don't update signals within their own computed callbacks
349
- 2. **Memory Leaks**: Clean up effects when components unmount
342
+ 2. **Memory Leaks**: Clean up effects when components unmount
350
343
  3. **Over-reactivity**: Structure data to minimize unnecessary updates
351
344
  4. **Async Race Conditions**: Trust automatic cancellation with AbortSignal
345
+ 5. **Circular Dependencies**: The graph detects and throws `CircularDependencyError`
346
+ 6. **Untracked `byKey()`/`at()` access**: On Store, List, and Collection, `byKey()`, `at()`, `keyAt()`, and `indexOfKey()` do **not** create graph edges. They are direct lookups that bypass structural tracking. An effect using only `collection.byKey('x')?.get()` will react to value changes of key `'x'`, but will **not** re-run if key `'x'` is added or removed. Use `get()`, `keys()`, or `length` to track structural changes.
352
347
 
353
348
  ## Advanced Patterns
354
349
 
@@ -360,8 +355,8 @@ type Events = {
360
355
  }
361
356
 
362
357
  const eventBus = createStore<Events>({
363
- userLogin: UNSET,
364
- userLogout: UNSET
358
+ userLogin: undefined as unknown as Events['userLogin'],
359
+ userLogout: undefined as unknown as Events['userLogout'],
365
360
  })
366
361
 
367
362
  const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
@@ -371,13 +366,13 @@ const emit = <K extends keyof Events>(event: K, data: Events[K]) => {
371
366
  const on = <K extends keyof Events>(event: K, callback: (data: Events[K]) => void) =>
372
367
  createEffect(() => {
373
368
  const data = eventBus[event].get()
374
- if (data !== UNSET) callback(data)
369
+ if (data != null) callback(data)
375
370
  })
376
371
  ```
377
372
 
378
373
  ### Data Processing Pipelines
379
374
  ```typescript
380
- const rawData = new List([{ id: 1, value: 10 }])
375
+ const rawData = createList([{ id: 1, value: 10 }], { keyConfig: item => String(item.id) })
381
376
  const processed = rawData
382
377
  .deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
383
378
  .deriveCollection(item => ({ ...item, formatted: `$${item.doubled}` }))
@@ -385,9 +380,9 @@ const processed = rawData
385
380
 
386
381
  ### Stable List Keys
387
382
  ```typescript
388
- const playlist = new List([
383
+ const playlist = createList([
389
384
  { id: 'track1', title: 'Song A' }
390
- ], track => track.id)
385
+ ], { keyConfig: track => track.id })
391
386
 
392
387
  const firstTrack = playlist.byKey('track1') // Persists through sorting
393
388
  playlist.sort((a, b) => a.title.localeCompare(b.title))