@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/.ai-context.md CHANGED
@@ -6,102 +6,157 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
6
6
 
7
7
  ## Core Architecture
8
8
 
9
+ ### Graph-Based Reactivity (`src/graph.ts`)
10
+
11
+ The reactive engine is a linked graph of source and sink nodes connected by `Edge` entries:
12
+ - **Sources** (StateNode) maintain a linked list of sink edges
13
+ - **Sinks** (MemoNode, TaskNode, EffectNode) maintain a linked list of source edges
14
+ - `link()` creates edges between sources and sinks during `.get()` calls
15
+ - `propagate()` flags sinks as dirty when sources change
16
+ - `flush()` processes queued effects after propagation
17
+ - `trimSources()` removes stale edges after recomputation
18
+
19
+ ### Node Types
20
+ ```
21
+ StateNode<T> — source-only with equality + guard (State, Sensor)
22
+ MemoNode<T> — source + sink (Memo, Store, List, Collection)
23
+ TaskNode<T> — source + sink + async (Task)
24
+ EffectNode — sink + owner (Effect)
25
+ Scope — owner-only (createScope)
26
+ ```
27
+
9
28
  ### Signal Types
10
- - **Signal**: Base interface with `.get()` method for value access
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
14
- - **Store**: Mutable signals for objects with individually reactive properties
15
- - **List**: Mutable signals for arrays with individually reactive items
16
- - **Collection**: Interface for reactive array-like collections (implemented by DerivedCollection)
17
- - **Effect**: Side effect handlers that react to signal changes
29
+ - **State** (`createState`): Mutable signals for primitive values and objects
30
+ - **Sensor** (`createSensor`): Read-only signals for external input streams with automatic state updates. Use `SKIP_EQUALITY` for mutable object observation.
31
+ - **Memo** (`createMemo`): Synchronous derived computations with memoization and reducer capabilities
32
+ - **Task** (`createTask`): Async derived computations with automatic abort/cancellation
33
+ - **Store** (`createStore`): Mutable object signals with individually reactive properties via Proxy
34
+ - **List** (`createList`): Mutable array signals with stable keys and reactive items
35
+ - **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
36
+ - **Effect** (`createEffect`): Side effect handlers that react to signal changes
18
37
 
19
38
  ### Key Principles
20
- 1. **Functional Programming**: Immutable by default, pure functions
21
- 2. **Type Safety**: Full TypeScript support with strict type constraints
22
- 3. **Performance**: Minimal re-computations through dependency tracking
23
- 4. **Async Support**: Built-in cancellation with AbortSignal
24
- 5. **Tree-shaking**: Optimized for minimal bundle size
39
+ 1. **Functional API**: All signals created via `create*()` factory functions (no classes)
40
+ 2. **Type Safety**: Full TypeScript support with strict type constraints (`T extends {}`)
41
+ 3. **Performance**: Flag-based dirty checking, linked-list edge traversal, batched flushing
42
+ 4. **Async Support**: Built-in cancellation with AbortSignal in Tasks
43
+ 5. **Tree-shaking**: Optimized for minimal bundle size with `/*#__PURE__*/` annotations
25
44
 
26
45
  ## Project Structure
27
46
 
28
47
  ```
29
48
  cause-effect/
30
49
  ├── src/
31
- │ ├── classes/
32
- ├── state.ts # Mutable state signals (State)
33
- │ │ ├── ref.ts # Signal wrapper for external objects (Ref)
34
- │ │ ├── store.ts # Object stores with reactive properties
35
- │ │ ├── list.ts # Array stores with stable keys (List)
36
- │ │ ├── collection.ts # Collection interface and DerivedCollection
37
- │ │ └── computed.ts # Computed/derived signals (Memo and Task)
38
- | ├── signal.ts # Base signal types and utilities
39
- │ ├── effect.ts # Effect system (createEffect)
40
- ├── system.ts # Core reactivity (watchers, batching, subscriptions)
41
- │ ├── resolve.ts # Helper for extracting signal values
42
- ├── match.ts # Pattern matching utility
43
- ├── diff.ts # Object comparison utilities
44
- ├── errors.ts # Custom error classes
45
- └── util.ts # Utilities and constants
46
- ├── index.ts # Main export file
47
- ├── types/index.d.ts # TypeScript declarations
48
- └── package.json # NPM package configuration
50
+ │ ├── graph.ts # Core reactive engine (nodes, edges, link, propagate, flush, batch)
51
+ │ ├── nodes/
52
+ │ │ ├── state.ts # createState mutable state signals
53
+ │ │ ├── sensor.ts # createSensor external input tracking (also covers mutable object observation)
54
+ │ │ ├── memo.ts # createMemo synchronous derived computations
55
+ │ │ ├── task.ts # createTask async derived computations
56
+ │ │ ├── effect.ts # createEffect, match side effects
57
+ ├── store.ts # createStore reactive object stores
58
+ ├── list.ts # createList reactive arrays with stable keys
59
+ │ └── collection.ts # createCollection externally-driven and derived collections
60
+ │ ├── util.ts # Utility functions and type checks
61
+ └── ...
62
+ ├── index.ts # Entry point / main export file
63
+ ├── test/
64
+ ├── state.test.ts
65
+ ├── memo.test.ts
66
+ ├── task.test.ts
67
+ │ ├── effect.test.ts
68
+ │ ├── store.test.ts
69
+ │ ├── list.test.ts
70
+ │ └── collection.test.ts
71
+ └── package.json
49
72
  ```
50
73
 
51
74
  ## API Patterns
52
75
 
53
76
  ### Signal Creation
54
77
  ```typescript
55
- // State signals for primitives
56
- const count = new State(42)
57
- const name = new State('Alice')
58
- const actions = new State<'increment' | 'decrement' | 'reset'>('reset')
78
+ // State for primitives and objects
79
+ const count = createState(42)
80
+ const name = createState('Alice')
81
+
82
+ // Sensor for external input
83
+ const mousePos = createSensor<{ x: number; y: number }>((set) => {
84
+ const handler = (e: MouseEvent) => set({ x: e.clientX, y: e.clientY })
85
+ window.addEventListener('mousemove', handler)
86
+ return () => window.removeEventListener('mousemove', handler)
87
+ })
59
88
 
60
- // Ref signals for external objects
61
- const elementRef = new Ref(document.getElementById('status'))
62
- const cacheRef = new Ref(new Map([['key', 'value']]))
89
+ // Sensor for mutable object observation (SKIP_EQUALITY)
90
+ const element = createSensor<HTMLElement>((set) => {
91
+ const node = document.getElementById('box')!
92
+ set(node)
93
+ const obs = new MutationObserver(() => set(node))
94
+ obs.observe(node, { attributes: true })
95
+ return () => obs.disconnect()
96
+ }, { value: node, equals: SKIP_EQUALITY })
63
97
 
64
- // Store signals for objects
98
+ // Store for objects with reactive properties
65
99
  const user = createStore({ name: 'Alice', age: 30 })
66
100
 
67
101
  // List with stable keys for arrays
68
- const items = new List(['apple', 'banana', 'cherry'])
69
- const users = new List([{ id: 'alice', name: 'Alice' }], user => user.id)
102
+ const items = createList(['apple', 'banana', 'cherry'])
103
+ const users = createList(
104
+ [{ id: 'alice', name: 'Alice' }],
105
+ { keyConfig: user => user.id }
106
+ )
70
107
 
71
- // Computed signals for derived values
72
- const doubled = new Memo(() => count.get() * 2)
108
+ // Memo for synchronous derived values
109
+ const doubled = createMemo(() => count.get() * 2)
73
110
 
74
- // Computed with reducer capabilities (access to previous value)
75
- const counter = new Memo((prev) => {
111
+ // Memo with reducer capabilities (access to previous value)
112
+ const counter = createMemo(prev => {
76
113
  const action = actions.get()
77
- switch (action) {
78
- case 'increment': return prev + 1
79
- case 'decrement': return prev - 1
80
- case 'reset': return 0
81
- default: return prev
82
- }
83
- }, 0) // Initial value
114
+ return action === 'increment' ? prev + 1 : prev - 1
115
+ }, { value: 0 })
84
116
 
85
- // Async computed with access to previous value
86
- const userData = new Task(async (prev, abort) => {
117
+ // Task for async derived values with cancellation
118
+ const userData = createTask(async (prev, abort) => {
87
119
  const id = userId.get()
88
- if (!id) return prev // Keep previous data if no user ID
120
+ if (!id) return prev
89
121
  const response = await fetch(`/users/${id}`, { signal: abort })
90
122
  return response.json()
91
123
  })
124
+
125
+ // Collection for derived transformations
126
+ const doubled = numbers.deriveCollection((value: number) => value * 2)
127
+ const enriched = users.deriveCollection(async (user, abort) => {
128
+ const res = await fetch(`/api/${user.id}`, { signal: abort })
129
+ return { ...user, details: await res.json() }
130
+ })
131
+
132
+ // Collection for externally-driven data
133
+ const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
134
+ const ws = new WebSocket('/feed')
135
+ ws.onmessage = (e) => applyChanges(JSON.parse(e.data))
136
+ return () => ws.close()
137
+ }, { keyConfig: item => item.id })
92
138
  ```
93
139
 
94
140
  ### Reactivity
95
141
  ```typescript
96
- // Effects react to signal changes
97
- createEffect(() => {
142
+ // Effects run when dependencies change
143
+ const dispose = createEffect(() => {
98
144
  console.log(`Count: ${count.get()}`)
99
145
  })
100
146
 
101
- // Async effects with automatic cancellation
102
- createEffect(async (abort) => {
103
- const response = await fetch('/api', { signal: abort })
104
- return response.json()
147
+ // Effects can return cleanup functions
148
+ createEffect(() => {
149
+ const timer = setInterval(() => console.log(count.get()), 1000)
150
+ return () => clearInterval(timer)
151
+ })
152
+
153
+ // match() for ergonomic signal value extraction inside effects
154
+ createEffect(() => {
155
+ match([userData], {
156
+ ok: ([data]) => updateUI(data),
157
+ nil: () => showLoading(),
158
+ err: errors => showError(errors[0].message)
159
+ })
105
160
  })
106
161
  ```
107
162
 
@@ -110,21 +165,24 @@ createEffect(async (abort) => {
110
165
  // All signals use .get() for value access
111
166
  const value = signal.get()
112
167
 
113
- // Mutable signals have .set() and .update()
168
+ // State has .set() and .update()
114
169
  state.set(newValue)
115
170
  state.update(current => current + 1)
116
171
 
117
- // Store properties are individually reactive
118
- user.name.set("Bob") // Only name watchers trigger
172
+ // Store properties are individually reactive via Proxy
173
+ user.name.set('Bob') // Only name watchers trigger
119
174
  user.age.update(age => age + 1) // Only age watchers trigger
120
175
 
121
- // List with stable keys for arrays
122
- const items = new List(['apple', 'banana'], 'fruit')
123
- const appleSignal = items.byKey('fruit0') // Access by stable key
124
- const firstKey = items.keyAt(0) // Get key at position
125
- const appleIndex = items.indexOfKey('fruit0') // Get position of key
126
- items.splice(1, 0, 'cherry') // Insert at position 1
127
- items.sort() // Sort while preserving keys
176
+ // List with stable keys
177
+ const items = createList(['apple', 'banana'], { keyConfig: 'fruit' })
178
+ const appleSignal = items.byKey('fruit0')
179
+ const firstKey = items.keyAt(0)
180
+ const appleIndex = items.indexOfKey('fruit0')
181
+ items.splice(1, 0, 'cherry')
182
+ items.sort()
183
+
184
+ // Collections via deriveCollection
185
+ const processed = items.deriveCollection(item => item.toUpperCase())
128
186
  ```
129
187
 
130
188
  ## Coding Conventions
@@ -132,194 +190,73 @@ items.sort() // Sort while preserving keys
132
190
  ### TypeScript Style
133
191
  - Generic constraints: `T extends {}` to exclude null/undefined
134
192
  - Use `const` for immutable values
135
- - Function overloads for complex type scenarios
193
+ - Function overloads for complex type scenarios (e.g., `createCollection`, `deriveCollection`)
136
194
  - JSDoc comments on all public APIs
137
195
  - Pure function annotations: `/*#__PURE__*/` for tree-shaking
138
196
 
139
197
  ### Naming Conventions
140
- - Factory functions: `create*` prefix (createEffect, createStore, createComputed)
141
- - Type predicates: `is*` prefix (isSignal, isState, isComputed)
142
- - Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_COMPUTED)
143
- - Utility constants: UPPER_CASE (UNSET)
198
+ - Factory functions: `create*` prefix (createState, createMemo, createEffect, createStore, createList, createCollection, createSensor)
199
+ - Type predicates: `is*` prefix (isState, isMemo, isTask, isStore, isList, isCollection, isSensor)
200
+ - Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_SENSOR, TYPE_COLLECTION)
201
+ - Callback types: `*Callback` suffix (MemoCallback, TaskCallback, EffectCallback, SensorCallback, CollectionCallback, DeriveCollectionCallback)
144
202
 
145
203
  ### Error Handling
146
- - Custom error classes in `src/errors.ts`
147
- - Descriptive error messages with context
148
- - Input validation at public API boundaries
149
- - Use `UNSET` symbol for uninitialized/pending states
204
+ - Custom error classes in `src/errors.ts`: CircularDependencyError, NullishSignalValueError, InvalidSignalValueError, InvalidCallbackError, RequiredOwnerError, UnsetSignalValueError
205
+ - Descriptive error messages with `[TypeName]` prefix
206
+ - Input validation via `validateSignalValue()` and `validateCallback()`
207
+ - Optional type guards via `guard` option in SignalOptions
150
208
 
151
209
  ## Performance Considerations
152
210
 
153
211
  ### Optimization Patterns
154
- - Dependency tracking with `Set<Watcher>` for O(1) operations
155
- - Batched updates to minimize effect re-runs
156
- - Shallow equality checks with `isEqual()` utility
157
- - Memoization in computed signals prevents unnecessary recalculations
158
- - Tree-shaking friendly with pure function annotations
212
+ - Linked-list edges for O(1) link/unlink operations
213
+ - Flag-based dirty checking: FLAG_CLEAN, FLAG_CHECK, FLAG_DIRTY, FLAG_RUNNING
214
+ - Batched updates via `batch()` to minimize effect re-runs
215
+ - Lazy evaluation: Memos only recompute when accessed and dirty
216
+ - Automatic abort of in-flight Tasks when sources change
159
217
 
160
218
  ### Memory Management
161
- - Automatic cleanup of watchers when effects are disposed
162
- - WeakMap usage for object-to-signal mappings where appropriate
219
+ - `trimSources()` removes stale edges after each recomputation
220
+ - `unlink()` calls `source.stop()` when last sink disconnects (auto-cleanup for Sensor/Collection/Store/List)
163
221
  - AbortSignal integration for canceling async operations
222
+ - `createScope()` for hierarchical cleanup of nested effects
164
223
 
165
- ## Common Implementation Patterns
224
+ ## Resource Management
166
225
 
167
- ### State Management
226
+ **Sensor and Collection** use a start callback that returns a Cleanup function:
168
227
  ```typescript
169
- // Simple state
170
- const loading = new State(false)
171
- const error = new State('') // Empty string means no error
172
-
173
- // Nested state with stores
174
- const appState = createStore({
175
- user: { id: 1, name: "Alice" },
176
- settings: { theme: "dark", notifications: true }
177
- })
178
-
179
- // Lists with stable keys for arrays
180
- const todoList = new List([
181
- { id: 'task1', text: 'Learn signals', completed: false },
182
- { id: 'task2', text: 'Build app', completed: false }
183
- ], todo => todo.id) // Use ID as stable key
184
-
185
- // Access todos by stable key for consistent references
186
- const firstTodo = todoList.byKey('task1')
187
- firstTodo?.completed.set(true) // Mark completed
188
-
189
- // Collections for read-only derived data
190
- const completedTodos = new DerivedCollection(todoList, todo =>
191
- todo.completed ? { ...todo, status: 'done' } : null
192
- ).filter(Boolean) // Remove null values
193
-
194
- // Async collections for enhanced data
195
- const todoWithDetails = new DerivedCollection(todoList, async (todo, abort) => {
196
- const response = await fetch(`/todos/${todo.id}/details`, { signal: abort })
197
- return { ...todo, details: await response.json() }
228
+ const sensor = createSensor<T>((set) => {
229
+ // setup input tracking, call set(value) to update
230
+ return () => { /* cleanup */ }
198
231
  })
199
232
 
200
- // Element collections for DOM reactivity
201
- const buttons = createElementCollection(document.body, 'button')
202
- const inputs = createElementCollection(form, 'input[type="text"]')
233
+ const feed = createCollection<T>((applyChanges) => {
234
+ // setup external data source, call applyChanges(diffResult) on changes
235
+ return () => { /* cleanup */ }
236
+ }, { keyConfig: item => item.id })
203
237
  ```
204
238
 
205
- ### Derived State
239
+ **Store and List** use an optional `watched` callback in options:
206
240
  ```typescript
207
- // Reducer pattern for state machines
208
- const appState = new Memo((prev) => {
209
- const event = events.get()
210
- switch (prev) {
211
- case 'idle':
212
- return event === 'start' ? 'loading' : 'idle'
213
- case 'loading':
214
- return event === 'success' ? 'loaded' : event === 'error' ? 'error' : 'loading'
215
- case 'loaded':
216
- return event === 'refresh' ? 'loading' : 'loaded'
217
- case 'error':
218
- return event === 'retry' ? 'loading' : 'error'
219
- default:
220
- return 'idle'
241
+ const store = createStore(initialValue, {
242
+ watched: () => {
243
+ // setup resources
244
+ return () => { /* cleanup */ }
221
245
  }
222
- }, 'idle') // Initial state
223
- ```
224
-
225
- ### Async Operations
226
- ```typescript
227
- // Computed with async data fetching
228
- const userData = new Task(async (prev, abort) => {
229
- const id = userId.get()
230
- if (!id) return prev // Retain previous data when no ID
231
- const response = await fetch(`/users/${id}`, { signal: abort })
232
- if (!response.ok) throw new Error('Failed to fetch user')
233
- return response.json()
234
246
  })
235
247
  ```
236
248
 
237
- ### Error Handling
238
- ```typescript
239
- // Handle loading/error states
240
- createEffect(() => {
241
- match(resolve({ userData }), {
242
- ok: ({ userData }) => console.log('User:', userData),
243
- nil: () => console.log('Loading...'),
244
- err: (errors) => console.error('Error:', errors[0])
245
- })
246
- })
247
- ```
248
-
249
- ### Helper Functions Usage
250
- ```typescript
251
- // Extract values from multiple signals
252
- const result = resolve({ name, age, email })
253
-
254
- // Pattern matching for discriminated unions
255
- match(result, {
256
- ok: (values) => console.log('Success:', values),
257
- nil: () => console.log('Some values pending'),
258
- err: (errors) => console.error('Errors:', errors)
259
- })
260
-
261
- // Object diffing
262
- const changes = diff(oldUser, newUser)
263
- console.log('Changed:', changes.change)
264
- console.log('Added:', changes.add)
265
- console.log('Removed:', changes.remove)
266
-
267
- // Collection transformations
268
- const processedItems = items.deriveCollection(item => ({
269
- ...item,
270
- processed: true,
271
- timestamp: Date.now()
272
- }))
273
-
274
- // Chained collections for data pipelines
275
- const finalResults = processedItems.deriveCollection(item =>
276
- item.processed ? { summary: `${item.name} at ${item.timestamp}` } : null
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
282
-
283
- // Resource management with watch hooks
284
- signal.on('watch', () => {
285
- console.log('Setting up resource...')
286
- const resource = createResource()
287
- return () => resource.cleanup() // Called when no effects watch this signal
288
- })
289
- ```
249
+ Resources activate on first sink link and cleanup when last sink unlinks.
290
250
 
291
251
  ## Build and Development
292
252
 
293
253
  ### Tools
294
254
  - **Runtime**: Bun (also works with Node.js)
295
255
  - **Build**: Bun build with TypeScript compilation
296
- - **Testing**: Bun test runner
256
+ - **Testing**: Bun test runner (`bun test`)
297
257
  - **Linting**: Biome for code formatting and linting
298
258
 
299
259
  ### Package Configuration
300
260
  - ES modules only (`"type": "module"`)
301
- - Dual exports: TypeScript source and compiled JavaScript
302
- - Tree-shaking friendly with proper sideEffects configuration
303
261
  - TypeScript declarations generated automatically
304
-
305
- ## Key Design Decisions
306
-
307
- ### Why Signals?
308
- - Fine-grained reactivity (update only what changed)
309
- - Explicit dependency tracking (no hidden dependencies)
310
- - Composable and testable
311
- - Framework agnostic
312
-
313
- ### Why TypeScript-First?
314
- - Better developer experience with autocomplete
315
- - Compile-time error catching
316
- - Self-documenting APIs
317
- - Better refactoring support
318
-
319
- ### Why Functional Approach?
320
- - Predictable behavior
321
- - Easier testing
322
- - Better composition
323
- - Minimal API surface
324
-
325
- When working with this codebase, focus on maintaining the established patterns, ensuring type safety, and optimizing for performance while keeping the API simple and predictable.
262
+ - Tree-shaking friendly with proper sideEffects configuration
package/.cursorrules CHANGED
@@ -1,58 +1,64 @@
1
- # Cause & Effect - Reactive State Management Library
1
+ # Cause & Effect - Reactive State Management Primitives
2
2
 
3
3
  ## Project Type
4
- TypeScript/JavaScript reactive state management library using signals pattern
4
+ TypeScript reactive state management primitives library using a unified signal graph
5
5
 
6
6
  ## Core Concepts
7
- - Signals: Base reactive primitives with .get() method
8
- - State: new State() for primitives/simple values
9
- - Ref: new Ref() for external objects (DOM, Map, Set) requiring manual .notify()
10
- - Computed: new Memo() for derived/memoized values and new Task() for asynchronous computations
11
- - Store: createStore() for objects with reactive properties
12
- - List: new List() for arrays with stable keys and reactive items
13
- - Collection: Interface for reactive collections (DerivedCollection, ElementCollection)
14
- - Effects: createEffect() for side effects
7
+ - Signals: Reactive primitives with .get() method for dependency tracking
8
+ - State: createState() for mutable source values
9
+ - Sensor: createSensor() for external input with lazy lifecycle
10
+ - Memo: createMemo() for synchronous memoized derivations
11
+ - Task: createTask() for asynchronous derivations with cancellation
12
+ - Store: createStore() for reactive objects with per-property signals
13
+ - List: createList() for reactive arrays with stable keys
14
+ - Collection: createCollection() for reactive collections (external source or derived, item-level memoization)
15
+ - Effect: createEffect() for side-effect sinks
15
16
 
16
17
  ## Code Style
17
18
  - Use const for immutable values
18
19
  - Generic constraints: T extends {} (excludes null/undefined)
19
20
  - Factory functions: create* prefix
20
- - Type predicates: is* prefix
21
+ - Type predicates: is* prefix
21
22
  - Constants: TYPE_* or UPPER_CASE
22
23
  - Pure functions marked with /*#__PURE__*/ comment
23
24
 
24
25
  ## Key Patterns
25
26
  - All signals have .get() method
26
27
  - Mutable signals have .set(value) and .update(fn)
27
- - Store properties auto-become reactive signals
28
- - Computed callbacks receive oldValue as first parameter for reducer patterns
29
- - Async computed and effect callbacks receive AbortSignal for async cancellation
30
- - Use UNSET symbol for uninitialized states
31
- - Batch updates for performance
32
- - JSDoc comments on all public APIs
28
+ - Store properties auto-become reactive signals via Proxy
29
+ - Memo callbacks receive previous value as first parameter for reducer patterns
30
+ - Task callbacks receive (next, AbortSignal)
31
+ - Sensor and Collection use start callbacks for lazy resource management
32
+ - Store and List support optional watched callbacks
33
+ - batch() for grouping multiple updates
34
+ - untrack() for reading signals without creating edges
33
35
 
34
36
  ## File Structure
35
- - src/signal.ts - Base signal types
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
42
- - src/effect.ts - Effect system
43
- - src/system.ts - Core reactivity (watchers, batching)
44
- - index.ts - Main exports
45
-
46
- ## Error Handling
37
+ - src/graph.ts - Core reactive engine (nodes, edges, propagation, flush)
38
+ - src/nodes/state.ts - Mutable state signals
39
+ - src/nodes/sensor.ts - External input tracking
40
+ - src/nodes/memo.ts - Synchronous derived signals
41
+ - src/nodes/task.ts - Asynchronous derived signals
42
+ - src/nodes/effect.ts - Side-effect system
43
+ - src/nodes/store.ts - Reactive objects
44
+ - src/nodes/list.ts - Reactive arrays with stable keys
45
+ - src/nodes/collection.ts - Externally-driven and derived collections
46
+ - next.ts - Public API exports
47
+
48
+ ## Error Handling
47
49
  - Custom error classes in src/errors.ts
48
- - Validate inputs, throw descriptive errors
49
- - Support AbortSignal in async operations
50
+ - Validate inputs with validateSignalValue() and validateCallback()
51
+ - AbortSignal integration for async cancellation in Task and Collection
50
52
 
51
53
  ## Performance
52
- - Use Set<Watcher> for subscriptions
53
- - Shallow equality with isEqual() utility
54
- - Tree-shaking friendly code
55
- - Minimize effect re-runs
54
+ - Linked-list edges for O(1) dependency tracking
55
+ - Flag-based dirty checking (CLEAN, CHECK, DIRTY)
56
+ - Tree-shaking friendly, zero dependencies
57
+ - Minimize effect re-runs via equality checks
58
+
59
+ ## Testing
60
+ - bun:test with describe/test/expect
61
+ - Test files: test/*.test.ts
56
62
 
57
63
  ## Build
58
64
  - Bun build tool and runtime