@zeix/cause-effect 0.17.3 → 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.
- package/.ai-context.md +163 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
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
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **Store
|
|
15
|
-
- **List
|
|
16
|
-
- **Collection
|
|
17
|
-
- **Effect
|
|
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
|
|
21
|
-
2. **Type Safety**: Full TypeScript support with strict type constraints
|
|
22
|
-
3. **Performance**:
|
|
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
|
-
│ ├──
|
|
32
|
-
│
|
|
33
|
-
│ │ ├──
|
|
34
|
-
│ │ ├──
|
|
35
|
-
│ │ ├──
|
|
36
|
-
│ │ ├──
|
|
37
|
-
│ │
|
|
38
|
-
|
|
39
|
-
│ ├──
|
|
40
|
-
│
|
|
41
|
-
│ ├──
|
|
42
|
-
│
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
│
|
|
46
|
-
├──
|
|
47
|
-
├──
|
|
48
|
-
|
|
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
|
|
56
|
-
const count =
|
|
57
|
-
const name =
|
|
58
|
-
|
|
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
|
-
//
|
|
61
|
-
const
|
|
62
|
-
const
|
|
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
|
|
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 =
|
|
69
|
-
const users =
|
|
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
|
-
//
|
|
72
|
-
const doubled =
|
|
108
|
+
// Memo for synchronous derived values
|
|
109
|
+
const doubled = createMemo(() => count.get() * 2)
|
|
73
110
|
|
|
74
|
-
//
|
|
75
|
-
const counter =
|
|
111
|
+
// Memo with reducer capabilities (access to previous value)
|
|
112
|
+
const counter = createMemo(prev => {
|
|
76
113
|
const action = actions.get()
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
86
|
-
const userData =
|
|
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
|
|
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
|
|
97
|
-
createEffect(() => {
|
|
142
|
+
// Effects run when dependencies change
|
|
143
|
+
const dispose = createEffect(() => {
|
|
98
144
|
console.log(`Count: ${count.get()}`)
|
|
99
145
|
})
|
|
100
146
|
|
|
101
|
-
//
|
|
102
|
-
createEffect(
|
|
103
|
-
const
|
|
104
|
-
return
|
|
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
|
-
//
|
|
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(
|
|
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
|
|
122
|
-
const items =
|
|
123
|
-
const appleSignal = items.byKey('fruit0')
|
|
124
|
-
const firstKey = items.keyAt(0)
|
|
125
|
-
const appleIndex = items.indexOfKey('fruit0')
|
|
126
|
-
items.splice(1, 0, 'cherry')
|
|
127
|
-
items.sort()
|
|
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,200 +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,
|
|
141
|
-
- Type predicates: `is*` prefix (
|
|
142
|
-
- Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE,
|
|
143
|
-
-
|
|
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
|
|
148
|
-
- Input validation
|
|
149
|
-
-
|
|
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
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
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
|
-
-
|
|
162
|
-
-
|
|
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
|
-
##
|
|
166
|
-
|
|
167
|
-
### State Management
|
|
168
|
-
```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() }
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
// Element collections for DOM reactivity
|
|
201
|
-
const buttons = createElementCollection(document.body, 'button')
|
|
202
|
-
const inputs = createElementCollection(form, 'input[type="text"]')
|
|
203
|
-
```
|
|
224
|
+
## Resource Management
|
|
204
225
|
|
|
205
|
-
|
|
226
|
+
**Sensor and Collection** use a start callback that returns a Cleanup function:
|
|
206
227
|
```typescript
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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'
|
|
221
|
-
}
|
|
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()
|
|
228
|
+
const sensor = createSensor<T>((set) => {
|
|
229
|
+
// setup input tracking, call set(value) to update
|
|
230
|
+
return () => { /* cleanup */ }
|
|
234
231
|
})
|
|
235
|
-
```
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
})
|
|
233
|
+
const feed = createCollection<T>((applyChanges) => {
|
|
234
|
+
// setup external data source, call applyChanges(diffResult) on changes
|
|
235
|
+
return () => { /* cleanup */ }
|
|
236
|
+
}, { keyConfig: item => item.id })
|
|
247
237
|
```
|
|
248
238
|
|
|
249
|
-
|
|
239
|
+
**Store and List** use an optional `watched` callback in options:
|
|
250
240
|
```typescript
|
|
251
|
-
|
|
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 callbacks
|
|
284
|
-
const endpoint = new State('https://api.example.com', {
|
|
241
|
+
const store = createStore(initialValue, {
|
|
285
242
|
watched: () => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
},
|
|
289
|
-
unwatched: () => {
|
|
290
|
-
console.log('Cleaning up API client...')
|
|
291
|
-
resource.cleanup()
|
|
243
|
+
// setup resources
|
|
244
|
+
return () => { /* cleanup */ }
|
|
292
245
|
}
|
|
293
246
|
})
|
|
294
|
-
|
|
295
247
|
```
|
|
296
248
|
|
|
249
|
+
Resources activate on first sink link and cleanup when last sink unlinks.
|
|
250
|
+
|
|
297
251
|
## Build and Development
|
|
298
252
|
|
|
299
253
|
### Tools
|
|
300
254
|
- **Runtime**: Bun (also works with Node.js)
|
|
301
255
|
- **Build**: Bun build with TypeScript compilation
|
|
302
|
-
- **Testing**: Bun test runner
|
|
256
|
+
- **Testing**: Bun test runner (`bun test`)
|
|
303
257
|
- **Linting**: Biome for code formatting and linting
|
|
304
258
|
|
|
305
259
|
### Package Configuration
|
|
306
260
|
- ES modules only (`"type": "module"`)
|
|
307
|
-
- Dual exports: TypeScript source and compiled JavaScript
|
|
308
|
-
- Tree-shaking friendly with proper sideEffects configuration
|
|
309
261
|
- TypeScript declarations generated automatically
|
|
310
|
-
|
|
311
|
-
## Key Design Decisions
|
|
312
|
-
|
|
313
|
-
### Why Signals?
|
|
314
|
-
- Fine-grained reactivity (update only what changed)
|
|
315
|
-
- Explicit dependency tracking (no hidden dependencies)
|
|
316
|
-
- Composable and testable
|
|
317
|
-
- Framework agnostic
|
|
318
|
-
|
|
319
|
-
### Why TypeScript-First?
|
|
320
|
-
- Better developer experience with autocomplete
|
|
321
|
-
- Compile-time error catching
|
|
322
|
-
- Self-documenting APIs
|
|
323
|
-
- Better refactoring support
|
|
324
|
-
|
|
325
|
-
### Why Functional Approach?
|
|
326
|
-
- Predictable behavior
|
|
327
|
-
- Easier testing
|
|
328
|
-
- Better composition
|
|
329
|
-
- Minimal API surface
|
|
330
|
-
|
|
331
|
-
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
|
|
1
|
+
# Cause & Effect - Reactive State Management Primitives
|
|
2
2
|
|
|
3
3
|
## Project Type
|
|
4
|
-
TypeScript
|
|
4
|
+
TypeScript reactive state management primitives library using a unified signal graph
|
|
5
5
|
|
|
6
6
|
## Core Concepts
|
|
7
|
-
- Signals:
|
|
8
|
-
- State:
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
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/
|
|
36
|
-
- src/
|
|
37
|
-
- src/
|
|
38
|
-
- src/
|
|
39
|
-
- src/
|
|
40
|
-
- src/
|
|
41
|
-
- src/
|
|
42
|
-
- src/
|
|
43
|
-
- src/
|
|
44
|
-
-
|
|
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
|
|
49
|
-
-
|
|
50
|
+
- Validate inputs with validateSignalValue() and validateCallback()
|
|
51
|
+
- AbortSignal integration for async cancellation in Task and Collection
|
|
50
52
|
|
|
51
53
|
## Performance
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
- Tree-shaking friendly
|
|
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
|