@zeix/cause-effect 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.ai-context.md +85 -21
  2. package/.cursorrules +11 -5
  3. package/.github/copilot-instructions.md +64 -13
  4. package/CLAUDE.md +143 -163
  5. package/LICENSE +1 -1
  6. package/README.md +248 -333
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +21 -21
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +139 -0
  12. package/{src → archive}/state.ts +13 -11
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +938 -509
  17. package/index.js +1 -1
  18. package/index.ts +50 -23
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +282 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +305 -0
  24. package/src/classes/ref.ts +68 -0
  25. package/src/classes/state.ts +98 -0
  26. package/src/classes/store.ts +210 -0
  27. package/src/diff.ts +26 -53
  28. package/src/effect.ts +9 -9
  29. package/src/errors.ts +71 -25
  30. package/src/match.ts +5 -12
  31. package/src/resolve.ts +3 -2
  32. package/src/signal.ts +58 -41
  33. package/src/system.ts +79 -42
  34. package/src/util.ts +16 -34
  35. package/test/batch.test.ts +15 -17
  36. package/test/benchmark.test.ts +4 -4
  37. package/test/collection.test.ts +853 -0
  38. package/test/computed.test.ts +138 -130
  39. package/test/diff.test.ts +2 -2
  40. package/test/effect.test.ts +36 -35
  41. package/test/list.test.ts +754 -0
  42. package/test/match.test.ts +25 -25
  43. package/test/ref.test.ts +227 -0
  44. package/test/resolve.test.ts +17 -19
  45. package/test/signal.test.ts +70 -119
  46. package/test/state.test.ts +44 -44
  47. package/test/store.test.ts +253 -929
  48. package/types/index.d.ts +12 -9
  49. package/types/src/classes/collection.d.ts +46 -0
  50. package/types/src/classes/composite.d.ts +15 -0
  51. package/types/src/classes/computed.d.ts +97 -0
  52. package/types/src/classes/list.d.ts +41 -0
  53. package/types/src/classes/ref.d.ts +39 -0
  54. package/types/src/classes/state.d.ts +52 -0
  55. package/types/src/classes/store.d.ts +51 -0
  56. package/types/src/diff.d.ts +8 -12
  57. package/types/src/errors.d.ts +17 -11
  58. package/types/src/signal.d.ts +27 -14
  59. package/types/src/system.d.ts +41 -20
  60. package/types/src/util.d.ts +6 -4
  61. package/src/store.ts +0 -474
  62. package/types/src/collection.d.ts +0 -26
  63. package/types/src/computed.d.ts +0 -33
  64. package/types/src/scheduler.d.ts +0 -55
  65. package/types/src/state.d.ts +0 -24
  66. package/types/src/store.d.ts +0 -65
package/.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
- - **Computed**: Read-only derived signals with automatic memoization, reducer-like capabilities and asyc handling
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
- │ ├── signal.ts # Base signal types and utilities
29
- │ ├── state.ts # Mutable state signals (createState)
30
- │ ├── store.ts # Object stores with reactive properties
31
- │ ├── computed.ts # Computed/derived signals (createComputed)
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 = createState(42)
50
- const name = createState('Alice')
51
- const actions = createState<'increment' | 'decrement' | 'reset'>('reset')
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 = createComputed(() => count.get() * 2)
72
+ const doubled = new Memo(() => count.get() * 2)
58
73
 
59
- // Computed with reducer-like capabilities (access to previous value)
60
- const counter = createComputed((prev) => {
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 = createComputed(async (prev, abort) => {
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 (createState, createStore, createComputed)
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 = createState(false)
148
- const error = createState('') // Empty string means no error
170
+ const loading = new State(false)
171
+ const error = new State('') // Empty string means no error
149
172
 
150
- // Complex state with stores
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-like pattern for state machines
160
- const appState = createComputed((currentState) => {
207
+ // Reducer pattern for state machines
208
+ const appState = new Memo((prev) => {
161
209
  const event = events.get()
162
- switch (currentState) {
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 = createComputed(async (oldValue, abort) => {
228
+ const userData = new Task(async (prev, abort) => {
181
229
  const id = userId.get()
182
- if (!id) return oldValue // Retain previous data when no ID
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: createState() for primitives/simple values
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
- - Computed: createComputed() for derived/memoized values
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/store.ts - Object stores
35
- - src/computed.ts - Computed signals
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 (`createState()`)
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
- - **Computed**: Derived read-only signals with memoization, reducer-like capabilities and async support (`createComputed()`)
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., `createState`, `createStore`)
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 = createState(42)
73
- const name = createState('Alice')
74
- const actions = createState<'increment' | 'decrement'>('increment')
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 = createComputed(() => count.get() * 2)
94
+ const doubled = new Memo(() => count.get() * 2)
81
95
 
82
- // Computed with reducer-like capabilities
83
- const counter = createComputed((prev) => {
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 = createComputed(async (prev, abort) => {
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