@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.
- package/.ai-context.md +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/.ai-context.md
CHANGED
|
@@ -9,8 +9,11 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
|
|
|
9
9
|
### Signal Types
|
|
10
10
|
- **Signal**: Base interface with `.get()` method for value access
|
|
11
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
|
|
12
14
|
- **Store**: Mutable signals for objects with individually reactive properties
|
|
13
|
-
- **
|
|
15
|
+
- **List**: Mutable signals for arrays with individually reactive items
|
|
16
|
+
- **Collection**: Interface for reactive array-like collections (implemented by DerivedCollection)
|
|
14
17
|
- **Effect**: Side effect handlers that react to signal changes
|
|
15
18
|
|
|
16
19
|
### Key Principles
|
|
@@ -25,10 +28,14 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
|
|
|
25
28
|
```
|
|
26
29
|
cause-effect/
|
|
27
30
|
├── src/
|
|
28
|
-
│ ├──
|
|
29
|
-
│ ├── state.ts # Mutable state signals (
|
|
30
|
-
│ ├──
|
|
31
|
-
│ ├──
|
|
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
|
|
32
39
|
│ ├── effect.ts # Effect system (createEffect)
|
|
33
40
|
│ ├── system.ts # Core reactivity (watchers, batching, subscriptions)
|
|
34
41
|
│ ├── resolve.ts # Helper for extracting signal values
|
|
@@ -46,18 +53,26 @@ cause-effect/
|
|
|
46
53
|
### Signal Creation
|
|
47
54
|
```typescript
|
|
48
55
|
// State signals for primitives
|
|
49
|
-
const count =
|
|
50
|
-
const name =
|
|
51
|
-
const actions =
|
|
56
|
+
const count = new State(42)
|
|
57
|
+
const name = new State('Alice')
|
|
58
|
+
const actions = new State<'increment' | 'decrement' | 'reset'>('reset')
|
|
59
|
+
|
|
60
|
+
// Ref signals for external objects
|
|
61
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
62
|
+
const cacheRef = new Ref(new Map([['key', 'value']]))
|
|
52
63
|
|
|
53
64
|
// Store signals for objects
|
|
54
65
|
const user = createStore({ name: 'Alice', age: 30 })
|
|
55
66
|
|
|
67
|
+
// 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)
|
|
70
|
+
|
|
56
71
|
// Computed signals for derived values
|
|
57
|
-
const doubled =
|
|
72
|
+
const doubled = new Memo(() => count.get() * 2)
|
|
58
73
|
|
|
59
|
-
// Computed with reducer
|
|
60
|
-
const counter =
|
|
74
|
+
// Computed with reducer capabilities (access to previous value)
|
|
75
|
+
const counter = new Memo((prev) => {
|
|
61
76
|
const action = actions.get()
|
|
62
77
|
switch (action) {
|
|
63
78
|
case 'increment': return prev + 1
|
|
@@ -68,7 +83,7 @@ const counter = createComputed((prev) => {
|
|
|
68
83
|
}, 0) // Initial value
|
|
69
84
|
|
|
70
85
|
// Async computed with access to previous value
|
|
71
|
-
const userData =
|
|
86
|
+
const userData = new Task(async (prev, abort) => {
|
|
72
87
|
const id = userId.get()
|
|
73
88
|
if (!id) return prev // Keep previous data if no user ID
|
|
74
89
|
const response = await fetch(`/users/${id}`, { signal: abort })
|
|
@@ -102,6 +117,14 @@ state.update(current => current + 1)
|
|
|
102
117
|
// Store properties are individually reactive
|
|
103
118
|
user.name.set("Bob") // Only name watchers trigger
|
|
104
119
|
user.age.update(age => age + 1) // Only age watchers trigger
|
|
120
|
+
|
|
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
|
|
105
128
|
```
|
|
106
129
|
|
|
107
130
|
## Coding Conventions
|
|
@@ -114,7 +137,7 @@ user.age.update(age => age + 1) // Only age watchers trigger
|
|
|
114
137
|
- Pure function annotations: `/*#__PURE__*/` for tree-shaking
|
|
115
138
|
|
|
116
139
|
### Naming Conventions
|
|
117
|
-
- Factory functions: `create*` prefix (
|
|
140
|
+
- Factory functions: `create*` prefix (createEffect, createStore, createComputed)
|
|
118
141
|
- Type predicates: `is*` prefix (isSignal, isState, isComputed)
|
|
119
142
|
- Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_COMPUTED)
|
|
120
143
|
- Utility constants: UPPER_CASE (UNSET)
|
|
@@ -144,22 +167,47 @@ user.age.update(age => age + 1) // Only age watchers trigger
|
|
|
144
167
|
### State Management
|
|
145
168
|
```typescript
|
|
146
169
|
// Simple state
|
|
147
|
-
const loading =
|
|
148
|
-
const error =
|
|
170
|
+
const loading = new State(false)
|
|
171
|
+
const error = new State('') // Empty string means no error
|
|
149
172
|
|
|
150
|
-
//
|
|
173
|
+
// Nested state with stores
|
|
151
174
|
const appState = createStore({
|
|
152
175
|
user: { id: 1, name: "Alice" },
|
|
153
176
|
settings: { theme: "dark", notifications: true }
|
|
154
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"]')
|
|
155
203
|
```
|
|
156
204
|
|
|
157
205
|
### Derived State
|
|
158
206
|
```typescript
|
|
159
|
-
// Reducer
|
|
160
|
-
const appState =
|
|
207
|
+
// Reducer pattern for state machines
|
|
208
|
+
const appState = new Memo((prev) => {
|
|
161
209
|
const event = events.get()
|
|
162
|
-
switch (
|
|
210
|
+
switch (prev) {
|
|
163
211
|
case 'idle':
|
|
164
212
|
return event === 'start' ? 'loading' : 'idle'
|
|
165
213
|
case 'loading':
|
|
@@ -177,9 +225,9 @@ const appState = createComputed((currentState) => {
|
|
|
177
225
|
### Async Operations
|
|
178
226
|
```typescript
|
|
179
227
|
// Computed with async data fetching
|
|
180
|
-
const userData =
|
|
228
|
+
const userData = new Task(async (prev, abort) => {
|
|
181
229
|
const id = userId.get()
|
|
182
|
-
if (!id) return
|
|
230
|
+
if (!id) return prev // Retain previous data when no ID
|
|
183
231
|
const response = await fetch(`/users/${id}`, { signal: abort })
|
|
184
232
|
if (!response.ok) throw new Error('Failed to fetch user')
|
|
185
233
|
return response.json()
|
|
@@ -215,6 +263,22 @@ const changes = diff(oldUser, newUser)
|
|
|
215
263
|
console.log('Changed:', changes.change)
|
|
216
264
|
console.log('Added:', changes.add)
|
|
217
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
|
|
218
282
|
```
|
|
219
283
|
|
|
220
284
|
## Build and Development
|
package/.cursorrules
CHANGED
|
@@ -5,9 +5,12 @@ TypeScript/JavaScript reactive state management library using signals pattern
|
|
|
5
5
|
|
|
6
6
|
## Core Concepts
|
|
7
7
|
- Signals: Base reactive primitives with .get() method
|
|
8
|
-
- State:
|
|
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
|
|
9
11
|
- Store: createStore() for objects with reactive properties
|
|
10
|
-
-
|
|
12
|
+
- List: new List() for arrays with stable keys and reactive items
|
|
13
|
+
- Collection: Interface for reactive collections (DerivedCollection, ElementCollection)
|
|
11
14
|
- Effects: createEffect() for side effects
|
|
12
15
|
|
|
13
16
|
## Code Style
|
|
@@ -30,9 +33,12 @@ TypeScript/JavaScript reactive state management library using signals pattern
|
|
|
30
33
|
|
|
31
34
|
## File Structure
|
|
32
35
|
- src/signal.ts - Base signal types
|
|
33
|
-
- src/state.ts - Mutable state signals
|
|
34
|
-
- src/
|
|
35
|
-
- src/
|
|
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
|
|
36
42
|
- src/effect.ts - Effect system
|
|
37
43
|
- src/system.ts - Core reactivity (watchers, batching)
|
|
38
44
|
- index.ts - Main exports
|
|
@@ -7,17 +7,23 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
7
7
|
## Core Architecture
|
|
8
8
|
|
|
9
9
|
- **Signals**: Base reactive primitives with `.get()` method
|
|
10
|
-
- **State**: Mutable signals for primitive values (`
|
|
10
|
+
- **State**: Mutable signals for primitive values (`new State()`)
|
|
11
|
+
- **Ref**: Signal wrappers for external objects that change outside reactive system (`new Ref()`)
|
|
12
|
+
- **Computed**: Derived read-only signals with memoization, reducer capabilities and async support (`new Memo()`, `new Task()`)
|
|
11
13
|
- **Store**: Mutable signals for objects with reactive properties (`createStore()`)
|
|
12
|
-
- **
|
|
14
|
+
- **List**: Mutable signals for arrays with stable keys and reactive entries (`new List()`)
|
|
15
|
+
- **Collection**: Interface for reactive collections (implemented by `DerivedCollection`)
|
|
13
16
|
- **Effects**: Side effect handlers that react to signal changes (`createEffect()`)
|
|
14
17
|
|
|
15
18
|
## Key Files Structure
|
|
16
19
|
|
|
20
|
+
- `src/classes/state.ts` - Mutable state signals
|
|
21
|
+
- `src/classes/ref.ts` - Signal wrappers for external objects (DOM, Map, Set, etc.)
|
|
22
|
+
- `src/classes/store.ts` - Object stores with reactive properties
|
|
23
|
+
- `src/classes/list.ts` - Array stores with stable keys and reactive items
|
|
24
|
+
- `src/classes/collection.ts` - Collection interface and DerivedCollection implementation
|
|
25
|
+
- `src/classes/computed.ts` - Computed/derived signals
|
|
17
26
|
- `src/signal.ts` - Base signal types and utilities
|
|
18
|
-
- `src/state.ts` - Mutable state signals
|
|
19
|
-
- `src/store.ts` - Object stores with reactive properties
|
|
20
|
-
- `src/computed.ts` - Computed/derived signals
|
|
21
27
|
- `src/effect.ts` - Effect system
|
|
22
28
|
- `src/system.ts` - Core reactivity system (watchers, batching)
|
|
23
29
|
- `src/util.ts` - Utility functions and constants
|
|
@@ -33,7 +39,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
33
39
|
- JSDoc comments for all public APIs
|
|
34
40
|
|
|
35
41
|
### Naming Conventions
|
|
36
|
-
- Factory functions: `create*` (e.g., `
|
|
42
|
+
- Factory functions: `create*` (e.g., `createEffect`, `createStore`)
|
|
37
43
|
- Type predicates: `is*` (e.g., `isSignal`, `isState`)
|
|
38
44
|
- Constants: `TYPE_*` for type tags, `UPPER_CASE` for values
|
|
39
45
|
- Private variables: use descriptive names, no underscore prefix
|
|
@@ -69,18 +75,26 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
|
|
|
69
75
|
### Creating Signals
|
|
70
76
|
```typescript
|
|
71
77
|
// State for primitives
|
|
72
|
-
const count =
|
|
73
|
-
const name =
|
|
74
|
-
const actions =
|
|
78
|
+
const count = new State(42)
|
|
79
|
+
const name = new State('Alice')
|
|
80
|
+
const actions = new State<'increment' | 'decrement'>('increment')
|
|
81
|
+
|
|
82
|
+
// Ref for external objects
|
|
83
|
+
const elementRef = new Ref(document.getElementById('status'))
|
|
84
|
+
const cacheRef = new Ref(new Map())
|
|
75
85
|
|
|
76
86
|
// Store for objects
|
|
77
87
|
const user = createStore({ name: 'Alice', age: 30 })
|
|
78
88
|
|
|
89
|
+
// List with stable keys for arrays
|
|
90
|
+
const items = new List(['apple', 'banana', 'cherry'])
|
|
91
|
+
const users = new List([{ id: 'alice', name: 'Alice' }], user => user.id)
|
|
92
|
+
|
|
79
93
|
// Computed for derived values
|
|
80
|
-
const doubled =
|
|
94
|
+
const doubled = new Memo(() => count.get() * 2)
|
|
81
95
|
|
|
82
|
-
// Computed with reducer
|
|
83
|
-
const counter =
|
|
96
|
+
// Computed with reducer capabilities
|
|
97
|
+
const counter = new Memo(prev => {
|
|
84
98
|
const action = actions.get()
|
|
85
99
|
return action === 'increment' ? prev + 1 : prev - 1
|
|
86
100
|
}, 0) // Initial value
|
|
@@ -100,7 +114,7 @@ createEffect(async (abort) => {
|
|
|
100
114
|
})
|
|
101
115
|
|
|
102
116
|
// Async computed with old value access
|
|
103
|
-
const userData =
|
|
117
|
+
const userData = new Task(async (prev, abort) => {
|
|
104
118
|
if (!userId.get()) return prev // Keep previous data if no user
|
|
105
119
|
const response = await fetch(`/users/${userId.get()}`, { signal: abort })
|
|
106
120
|
return response.json()
|
|
@@ -122,6 +136,43 @@ function isSignal<T extends {}>(value: unknown): value is Signal<T>
|
|
|
122
136
|
- Minified production build
|
|
123
137
|
- ES modules only (`"type": "module"`)
|
|
124
138
|
|
|
139
|
+
## Store Methods and Stable Keys
|
|
140
|
+
|
|
141
|
+
### List Methods
|
|
142
|
+
- `byKey(key)` - Access signals by stable key instead of index
|
|
143
|
+
- `keyAt(index)` - Get stable key at specific position
|
|
144
|
+
- `indexOfKey(key)` - Get position of stable key in current order
|
|
145
|
+
- `splice(start, deleteCount, ...items)` - Array-like splicing with stable key preservation
|
|
146
|
+
- `sort(compareFn)` - Sort items while maintaining stable key associations
|
|
147
|
+
|
|
148
|
+
### Stable Keys Usage
|
|
149
|
+
```typescript
|
|
150
|
+
// Default auto-incrementing keys
|
|
151
|
+
const items = new List(['a', 'b', 'c'])
|
|
152
|
+
|
|
153
|
+
// String prefix keys
|
|
154
|
+
const items = new List(['apple', 'banana'], 'fruit')
|
|
155
|
+
// Creates keys: 'fruit0', 'fruit1'
|
|
156
|
+
|
|
157
|
+
// Function-based keys
|
|
158
|
+
const users = new List([
|
|
159
|
+
{ id: 'user1', name: 'Alice' },
|
|
160
|
+
{ id: 'user2', name: 'Bob' }
|
|
161
|
+
], user => user.id) // Uses user.id as stable key
|
|
162
|
+
|
|
163
|
+
// Collections derived from lists
|
|
164
|
+
const userProfiles = new DerivedCollection(users, user => ({
|
|
165
|
+
...user,
|
|
166
|
+
displayName: `${user.name} (ID: ${user.id})`
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
// Chained collections for data pipelines
|
|
170
|
+
const activeUserSummaries = users
|
|
171
|
+
.deriveCollection(user => ({ ...user, active: user.lastLogin > Date.now() - 86400000 }))
|
|
172
|
+
.deriveCollection(user => user.active ? `Active: ${user.name}` : null)
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
```
|
|
175
|
+
|
|
125
176
|
## When suggesting code:
|
|
126
177
|
1. Follow the established patterns for signal creation and usage
|
|
127
178
|
2. Use proper TypeScript types and generics
|