@zeix/cause-effect 0.16.1 → 0.17.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 (61) hide show
  1. package/.ai-context.md +71 -21
  2. package/.cursorrules +3 -2
  3. package/.github/copilot-instructions.md +59 -13
  4. package/CLAUDE.md +170 -24
  5. package/LICENSE +1 -1
  6. package/README.md +156 -52
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +19 -19
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +138 -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 +899 -503
  17. package/index.js +1 -1
  18. package/index.ts +41 -22
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +272 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +304 -0
  24. package/src/classes/state.ts +98 -0
  25. package/src/classes/store.ts +210 -0
  26. package/src/diff.ts +26 -53
  27. package/src/effect.ts +9 -9
  28. package/src/errors.ts +50 -25
  29. package/src/signal.ts +58 -41
  30. package/src/system.ts +79 -42
  31. package/src/util.ts +16 -30
  32. package/test/batch.test.ts +15 -17
  33. package/test/benchmark.test.ts +4 -4
  34. package/test/collection.test.ts +796 -0
  35. package/test/computed.test.ts +138 -130
  36. package/test/diff.test.ts +2 -2
  37. package/test/effect.test.ts +36 -35
  38. package/test/list.test.ts +754 -0
  39. package/test/match.test.ts +25 -25
  40. package/test/resolve.test.ts +17 -19
  41. package/test/signal.test.ts +70 -119
  42. package/test/state.test.ts +44 -44
  43. package/test/store.test.ts +253 -929
  44. package/types/index.d.ts +10 -8
  45. package/types/src/classes/collection.d.ts +32 -0
  46. package/types/src/classes/composite.d.ts +15 -0
  47. package/types/src/classes/computed.d.ts +97 -0
  48. package/types/src/classes/list.d.ts +41 -0
  49. package/types/src/classes/state.d.ts +52 -0
  50. package/types/src/classes/store.d.ts +51 -0
  51. package/types/src/diff.d.ts +8 -12
  52. package/types/src/errors.d.ts +12 -11
  53. package/types/src/signal.d.ts +27 -14
  54. package/types/src/system.d.ts +41 -20
  55. package/types/src/util.d.ts +6 -3
  56. package/src/store.ts +0 -474
  57. package/types/src/collection.d.ts +0 -26
  58. package/types/src/computed.d.ts +0 -33
  59. package/types/src/scheduler.d.ts +0 -55
  60. package/types/src/state.d.ts +0 -24
  61. package/types/src/store.d.ts +0 -65
package/.ai-context.md CHANGED
@@ -10,7 +10,9 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
10
10
  - **Signal**: Base interface with `.get()` method for value access
11
11
  - **State**: Mutable signals for primitive values (numbers, strings, booleans)
12
12
  - **Store**: Mutable signals for objects with individually reactive properties
13
- - **Computed**: Read-only derived signals with automatic memoization, reducer-like capabilities and asyc handling
13
+ - **List**: Mutable signals for arrays with individually reactive items
14
+ - **Collection**: Read-only derived arrays with item-level memoization and async support
15
+ - **Computed**: Read-only derived signals with automatic memoization, reducer capabilities and asyc handling
14
16
  - **Effect**: Side effect handlers that react to signal changes
15
17
 
16
18
  ### Key Principles
@@ -25,10 +27,13 @@ Cause & Effect is a modern reactive state management library for JavaScript/Type
25
27
  ```
26
28
  cause-effect/
27
29
  ├── 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)
30
+ │ ├── classes/
31
+ ├── state.ts # Mutable state signals (State)
32
+ ├── store.ts # Object stores with reactive properties
33
+ ├── list.ts # Array stores with stable keys (List)
34
+ │ │ ├── collection.ts # Read-only derived arrays (Collection)
35
+ │ │ └── computed.ts # Computed/derived signals (Memo and Task)
36
+ | ├── signal.ts # Base signal types and utilities
32
37
  │ ├── effect.ts # Effect system (createEffect)
33
38
  │ ├── system.ts # Core reactivity (watchers, batching, subscriptions)
34
39
  │ ├── resolve.ts # Helper for extracting signal values
@@ -46,18 +51,22 @@ cause-effect/
46
51
  ### Signal Creation
47
52
  ```typescript
48
53
  // State signals for primitives
49
- const count = createState(42)
50
- const name = createState('Alice')
51
- const actions = createState<'increment' | 'decrement' | 'reset'>('reset')
54
+ const count = new State(42)
55
+ const name = new State('Alice')
56
+ const actions = new State<'increment' | 'decrement' | 'reset'>('reset')
52
57
 
53
58
  // Store signals for objects
54
59
  const user = createStore({ name: 'Alice', age: 30 })
55
60
 
61
+ // List with stable keys for arrays
62
+ const items = new List(['apple', 'banana', 'cherry'])
63
+ const users = new List([{ id: 'alice', name: 'Alice' }], user => user.id)
64
+
56
65
  // Computed signals for derived values
57
- const doubled = createComputed(() => count.get() * 2)
66
+ const doubled = new Memo(() => count.get() * 2)
58
67
 
59
- // Computed with reducer-like capabilities (access to previous value)
60
- const counter = createComputed((prev) => {
68
+ // Computed with reducer capabilities (access to previous value)
69
+ const counter = new Memo((prev) => {
61
70
  const action = actions.get()
62
71
  switch (action) {
63
72
  case 'increment': return prev + 1
@@ -68,7 +77,7 @@ const counter = createComputed((prev) => {
68
77
  }, 0) // Initial value
69
78
 
70
79
  // Async computed with access to previous value
71
- const userData = createComputed(async (prev, abort) => {
80
+ const userData = new Task(async (prev, abort) => {
72
81
  const id = userId.get()
73
82
  if (!id) return prev // Keep previous data if no user ID
74
83
  const response = await fetch(`/users/${id}`, { signal: abort })
@@ -102,6 +111,14 @@ state.update(current => current + 1)
102
111
  // Store properties are individually reactive
103
112
  user.name.set("Bob") // Only name watchers trigger
104
113
  user.age.update(age => age + 1) // Only age watchers trigger
114
+
115
+ // List with stable keys for arrays
116
+ const items = new List(['apple', 'banana'], 'fruit')
117
+ const appleSignal = items.byKey('fruit0') // Access by stable key
118
+ const firstKey = items.keyAt(0) // Get key at position
119
+ const appleIndex = items.indexOfKey('fruit0') // Get position of key
120
+ items.splice(1, 0, 'cherry') // Insert at position 1
121
+ items.sort() // Sort while preserving keys
105
122
  ```
106
123
 
107
124
  ## Coding Conventions
@@ -114,7 +131,7 @@ user.age.update(age => age + 1) // Only age watchers trigger
114
131
  - Pure function annotations: `/*#__PURE__*/` for tree-shaking
115
132
 
116
133
  ### Naming Conventions
117
- - Factory functions: `create*` prefix (createState, createStore, createComputed)
134
+ - Factory functions: `create*` prefix (createEffect, createStore, createComputed)
118
135
  - Type predicates: `is*` prefix (isSignal, isState, isComputed)
119
136
  - Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_COMPUTED)
120
137
  - Utility constants: UPPER_CASE (UNSET)
@@ -144,22 +161,43 @@ user.age.update(age => age + 1) // Only age watchers trigger
144
161
  ### State Management
145
162
  ```typescript
146
163
  // Simple state
147
- const loading = createState(false)
148
- const error = createState('') // Empty string means no error
164
+ const loading = new State(false)
165
+ const error = new State('') // Empty string means no error
149
166
 
150
- // Complex state with stores
167
+ // Nested state with stores
151
168
  const appState = createStore({
152
169
  user: { id: 1, name: "Alice" },
153
170
  settings: { theme: "dark", notifications: true }
154
171
  })
172
+
173
+ // Lists with stable keys for arrays
174
+ const todoList = new List([
175
+ { id: 'task1', text: 'Learn signals', completed: false },
176
+ { id: 'task2', text: 'Build app', completed: false }
177
+ ], todo => todo.id) // Use ID as stable key
178
+
179
+ // Access todos by stable key for consistent references
180
+ const firstTodo = todoList.byKey('task1')
181
+ firstTodo?.completed.set(true) // Mark completed
182
+
183
+ // Collections for read-only derived data
184
+ const completedTodos = new Collection(todoList, todo =>
185
+ todo.completed ? { ...todo, status: 'done' } : null
186
+ ).filter(Boolean) // Remove null values
187
+
188
+ // Async collections for enhanced data
189
+ const todoWithDetails = new Collection(todoList, async (todo, abort) => {
190
+ const response = await fetch(`/todos/${todo.id}/details`, { signal: abort })
191
+ return { ...todo, details: await response.json() }
192
+ })
155
193
  ```
156
194
 
157
195
  ### Derived State
158
196
  ```typescript
159
- // Reducer-like pattern for state machines
160
- const appState = createComputed((currentState) => {
197
+ // Reducer pattern for state machines
198
+ const appState = new Memo((prev) => {
161
199
  const event = events.get()
162
- switch (currentState) {
200
+ switch (prev) {
163
201
  case 'idle':
164
202
  return event === 'start' ? 'loading' : 'idle'
165
203
  case 'loading':
@@ -177,9 +215,9 @@ const appState = createComputed((currentState) => {
177
215
  ### Async Operations
178
216
  ```typescript
179
217
  // Computed with async data fetching
180
- const userData = createComputed(async (oldValue, abort) => {
218
+ const userData = new Task(async (prev, abort) => {
181
219
  const id = userId.get()
182
- if (!id) return oldValue // Retain previous data when no ID
220
+ if (!id) return prev // Retain previous data when no ID
183
221
  const response = await fetch(`/users/${id}`, { signal: abort })
184
222
  if (!response.ok) throw new Error('Failed to fetch user')
185
223
  return response.json()
@@ -215,6 +253,18 @@ const changes = diff(oldUser, newUser)
215
253
  console.log('Changed:', changes.change)
216
254
  console.log('Added:', changes.add)
217
255
  console.log('Removed:', changes.remove)
256
+
257
+ // Collection transformations
258
+ const processedItems = items.deriveCollection(item => ({
259
+ ...item,
260
+ processed: true,
261
+ timestamp: Date.now()
262
+ }))
263
+
264
+ // Chained collections for data pipelines
265
+ const finalResults = processedItems.deriveCollection(item =>
266
+ item.processed ? { summary: `${item.name} at ${item.timestamp}` } : null
267
+ ).filter(Boolean)
218
268
  ```
219
269
 
220
270
  ## Build and Development
package/.cursorrules CHANGED
@@ -5,9 +5,10 @@ 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
+ - Computed: new Memo() for derived/memoized values and new Task() for asynchronous computations
9
10
  - Store: createStore() for objects with reactive properties
10
- - Computed: createComputed() for derived/memoized values
11
+ - List: new List() for arrays with stable keys and reactive items
11
12
  - Effects: createEffect() for side effects
12
13
 
13
14
  ## Code Style
@@ -7,17 +7,21 @@ 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
+ - **Computed**: Derived read-only signals with memoization, reducer capabilities and async support (`new Memo()`, `new Task()`)
11
12
  - **Store**: Mutable signals for objects with reactive properties (`createStore()`)
12
- - **Computed**: Derived read-only signals with memoization, reducer-like capabilities and async support (`createComputed()`)
13
+ - **List**: Mutable signals for arrays with stable keys and reactive entries (`new List()`)
14
+ - **Collection**: Read-only derived arrays with item-level memoization and async support (`new Collection()`)
13
15
  - **Effects**: Side effect handlers that react to signal changes (`createEffect()`)
14
16
 
15
17
  ## Key Files Structure
16
18
 
19
+ - `src/classes/state.ts` - Mutable state signals
20
+ - `src/classes/store.ts` - Object stores with reactive properties
21
+ - `src/classes/list.ts` - Array stores with stable keys and reactive items
22
+ - `src/classes/collection.ts` - Read-only derived arrays with memoization
23
+ - `src/classes/computed.ts` - Computed/derived signals
17
24
  - `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
25
  - `src/effect.ts` - Effect system
22
26
  - `src/system.ts` - Core reactivity system (watchers, batching)
23
27
  - `src/util.ts` - Utility functions and constants
@@ -33,7 +37,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
33
37
  - JSDoc comments for all public APIs
34
38
 
35
39
  ### Naming Conventions
36
- - Factory functions: `create*` (e.g., `createState`, `createStore`)
40
+ - Factory functions: `create*` (e.g., `createEffect`, `createStore`)
37
41
  - Type predicates: `is*` (e.g., `isSignal`, `isState`)
38
42
  - Constants: `TYPE_*` for type tags, `UPPER_CASE` for values
39
43
  - Private variables: use descriptive names, no underscore prefix
@@ -69,18 +73,22 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
69
73
  ### Creating Signals
70
74
  ```typescript
71
75
  // State for primitives
72
- const count = createState(42)
73
- const name = createState('Alice')
74
- const actions = createState<'increment' | 'decrement'>('increment')
76
+ const count = new State(42)
77
+ const name = new State('Alice')
78
+ const actions = new State<'increment' | 'decrement'>('increment')
75
79
 
76
80
  // Store for objects
77
81
  const user = createStore({ name: 'Alice', age: 30 })
78
82
 
83
+ // List with stable keys for arrays
84
+ const items = new List(['apple', 'banana', 'cherry'])
85
+ const users = new List([{ id: 'alice', name: 'Alice' }], user => user.id)
86
+
79
87
  // Computed for derived values
80
- const doubled = createComputed(() => count.get() * 2)
88
+ const doubled = new Memo(() => count.get() * 2)
81
89
 
82
- // Computed with reducer-like capabilities
83
- const counter = createComputed((prev) => {
90
+ // Computed with reducer capabilities
91
+ const counter = new Memo(prev => {
84
92
  const action = actions.get()
85
93
  return action === 'increment' ? prev + 1 : prev - 1
86
94
  }, 0) // Initial value
@@ -100,7 +108,7 @@ createEffect(async (abort) => {
100
108
  })
101
109
 
102
110
  // Async computed with old value access
103
- const userData = createComputed(async (prev, abort) => {
111
+ const userData = new Task(async (prev, abort) => {
104
112
  if (!userId.get()) return prev // Keep previous data if no user
105
113
  const response = await fetch(`/users/${userId.get()}`, { signal: abort })
106
114
  return response.json()
@@ -122,6 +130,44 @@ function isSignal<T extends {}>(value: unknown): value is Signal<T>
122
130
  - Minified production build
123
131
  - ES modules only (`"type": "module"`)
124
132
 
133
+ ## Store Methods and Stable Keys
134
+
135
+ ### List Methods
136
+ - `byKey(key)` - Access signals by stable key instead of index
137
+ - `keyAt(index)` - Get stable key at specific position
138
+ - `indexOfKey(key)` - Get position of stable key in current order
139
+ - `splice(start, deleteCount, ...items)` - Array-like splicing with stable key preservation
140
+ - `sort(compareFn)` - Sort items while maintaining stable key associations
141
+
142
+ ### Stable Keys Usage
143
+ ```typescript
144
+ // Default auto-incrementing keys
145
+ const items = new List(['a', 'b', 'c'])
146
+
147
+ // String prefix keys
148
+ // Lists with stable keys
149
+ const items = new List(['apple', 'banana'], 'fruit')
150
+ // Creates keys: 'fruit0', 'fruit1'
151
+
152
+ // Function-based keys
153
+ const users = new List([
154
+ { id: 'user1', name: 'Alice' },
155
+ { id: 'user2', name: 'Bob' }
156
+ ], user => user.id) // Uses user.id as stable key
157
+
158
+ // Collections derived from lists
159
+ const userProfiles = new Collection(users, user => ({
160
+ ...user,
161
+ displayName: `${user.name} (ID: ${user.id})`
162
+ }))
163
+
164
+ // Chained collections for data pipelines
165
+ const activeUserSummaries = users
166
+ .deriveCollection(user => ({ ...user, active: user.lastLogin > Date.now() - 86400000 }))
167
+ .deriveCollection(user => user.active ? `Active: ${user.name}` : null)
168
+ .filter(Boolean)
169
+ ```
170
+
125
171
  ## When suggesting code:
126
172
  1. Follow the established patterns for signal creation and usage
127
173
  2. Use proper TypeScript types and generics
package/CLAUDE.md CHANGED
@@ -17,6 +17,7 @@ Think of signals as **observable cells** in a spreadsheet:
17
17
  - **State signals** are like input cells where you can directly enter values
18
18
  - **Computed signals** are like formula cells that automatically recalculate when their dependencies change
19
19
  - **Store signals** are like structured data tables where individual columns (properties) are reactive
20
+ - **List signals** with stable keys are like tables with persistent row IDs that survive sorting and reordering
20
21
  - **Effects** are like event handlers that trigger side effects when cells change
21
22
 
22
23
  The key insight is that the system tracks which cells (signals) are read during computation, creating an automatic dependency graph that ensures minimal and correct updates.
@@ -43,13 +44,44 @@ interface MutableSignal<T extends {}> extends Signal<T> {
43
44
  set(value: T): void
44
45
  update(fn: (current: T) => T): void
45
46
  }
47
+
48
+ // Collection signals are read-only derived arrays
49
+ interface Collection<T extends {}, U extends {}> extends Signal<T[]> {
50
+ at(index: number): Computed<T> | undefined
51
+ byKey(key: string): Computed<T> | undefined
52
+ deriveCollection<R extends {}>(callback: CollectionCallback<R, T>): Collection<R, T>
53
+ }
46
54
  ```
47
55
 
48
56
  The generic constraint `T extends {}` is crucial - it excludes `null` and `undefined` at the type level, preventing common runtime errors and making the API more predictable.
49
57
 
58
+ ### Collection Signal Deep Dive
59
+
60
+ Collection signals provide read-only derived array transformations with automatic memoization:
61
+
62
+ 1. **Source Dependency**: Collections derive from Lists or other Collections
63
+ 2. **Item-level Memoization**: Each item transformation is individually memoized
64
+ 3. **Async Support**: Supports Promise-based transformations with cancellation
65
+ 4. **Stable Keys**: Maintains the same stable key system as source Lists
66
+ 5. **Chainable**: Collections can derive from other Collections for data pipelines
67
+
68
+ Key implementation details:
69
+ - Individual items are Computed signals, not mutable signals
70
+ - Automatically handles source List changes (add, remove, sort)
71
+ - Supports both sync and async transformation callbacks
72
+ - Lazy evaluation - transformations only run when accessed
73
+ - Smart watcher management - only creates watchers when change listeners are active
74
+ - Order preservation through internal `#order` array synchronized with source
75
+
76
+ Collection Architecture:
77
+ - **BaseCollection**: Core implementation with signal management and event handling
78
+ - **Computed Creation**: Each source item gets its own computed signal with transformation callback
79
+ - **Source Synchronization**: Listens to source events and maintains parallel structure
80
+ - **Memory Efficiency**: Automatically cleans up computed signals when source items are removed
81
+
50
82
  ### Store Signal Deep Dive
51
83
 
52
- Store signals are the most complex part of the system. They transform plain objects into reactive data structures:
84
+ Store signals are the most complex part of the system. They transform plain objects or arrays into reactive data structures:
53
85
 
54
86
  1. **Property Proxification**: Each property becomes its own signal
55
87
  2. **Nested Reactivity**: Objects within objects recursively become stores
@@ -61,6 +93,24 @@ Key implementation details:
61
93
  - Maintains internal signal instances for each property
62
94
  - Supports change notifications for fine-grained updates
63
95
  - Handles edge cases like symbol properties and prototype chain
96
+ - For lists: maintains stable keys that persist through sorting, splicing, and reordering
97
+ - Provides specialized methods: `byKey()`, `keyAt()`, `indexOfKey()`, `splice()` for array manipulation
98
+
99
+ ### MutableComposite Architecture
100
+
101
+ Both Store and List signals are built on top of `MutableComposite`, which provides the common reactive property management:
102
+
103
+ 1. **Signal Management**: Maintains a `Map<string, MutableSignal>` of property signals
104
+ 2. **Change Detection**: Uses `diff()` to detect actual changes before triggering updates
105
+ 3. **Event System**: Emits granular notifications for add/change/remove operations
106
+ 4. **Validation**: Enforces type constraints and prevents invalid mutations
107
+ 5. **Performance**: Only creates signals when properties are first accessed or added
108
+
109
+ Key implementation details:
110
+ - Lazy signal creation for better memory usage
111
+ - Efficient batch updates through change detection
112
+ - Support for both object and array data structures
113
+ - Automatic cleanup when properties are removed
64
114
 
65
115
  ### Computed Signal Memoization Strategy
66
116
 
@@ -69,22 +119,22 @@ Computed signals implement smart memoization:
69
119
  - **Stale Detection**: Only recalculates when dependencies actually change
70
120
  - **Async Support**: Handles Promise-based computations with automatic cancellation
71
121
  - **Error Handling**: Preserves error states and prevents cascade failures
72
- - **Reducer-like Capabilities**: Access to previous value enables state accumulation and transitions
122
+ - **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
73
123
 
74
124
  ## Advanced Patterns and Best Practices
75
125
 
76
126
  ### When to Use Each Signal Type
77
127
 
78
- **State Signals (`createState`)**:
128
+ **State Signals (`State`)**:
79
129
  - Primitive values (numbers, strings, booleans)
80
130
  - Objects that you replace entirely rather than mutating properties
81
131
  - Simple toggles and flags
82
132
  - Values with straightforward update patterns
83
133
 
84
134
  ```typescript
85
- const count = createState(0)
86
- const theme = createState<'light' | 'dark'>('light')
87
- const user = createState<User>({ id: 1, name: 'John Doe', email: 'john@example.com' }) // Replace entire user object
135
+ const count = new State(0)
136
+ const theme = new State<'light' | 'dark'>('light')
137
+ const user = new State<User>({ id: 1, name: 'John Doe', email: 'john@example.com' }) // Replace entire user object
88
138
  ```
89
139
 
90
140
  **Store Signals (`createStore`)**:
@@ -102,28 +152,69 @@ const form = createStore({
102
152
  // form.email.set('user@example.com') // Only email subscribers react
103
153
  ```
104
154
 
105
- **Computed Signals (`createComputed`)**:
155
+ **List Signals (`new List`)**:
156
+
157
+ ```ts
158
+ // Lists with stable keys
159
+ const todoList = new List([
160
+ { id: 'task1', text: 'Learn signals' },
161
+ { id: 'task2', text: 'Build app' }
162
+ ], todo => todo.id) // Use todo.id as stable key
163
+
164
+ // Access by stable key instead of index
165
+ const firstTodo = todoList.byKey('task1')
166
+ firstTodo?.text.set('Learn signals deeply')
167
+
168
+ // Sort while maintaining stable references
169
+ todoList.sort((a, b) => a.text.localeCompare(b.text))
170
+ ```
171
+
172
+ **Collection Signals (`new Collection`)**:
173
+
174
+ ```ts
175
+ // Read-only derived arrays with memoization
176
+ const completedTodos = new Collection(todoList, todo =>
177
+ todo.completed ? { ...todo, status: 'done' } : null
178
+ )
179
+
180
+ // Async transformations with cancellation
181
+ const todoDetails = new Collection(todoList, async (todo, abort) => {
182
+ const response = await fetch(`/todos/${todo.id}`, { signal: abort })
183
+ return { ...todo, details: await response.json() }
184
+ })
185
+
186
+ // Chain collections for data pipelines
187
+ const urgentTodoSummaries = todoList
188
+ .deriveCollection(todo => ({ ...todo, urgent: todo.priority > 8 }))
189
+ .deriveCollection(todo => todo.urgent ? `URGENT: ${todo.text}` : todo.text)
190
+
191
+ // Collections maintain stable references through List changes
192
+ const firstTodoDetail = todoDetails.byKey('task1') // Computed signal
193
+ todoList.sort() // Reorders list but collection signals remain stable
194
+ ```
195
+
196
+ **Computed Signals (`Memo` and `Task`)**:
106
197
  - Expensive calculations that should be memoized
107
198
  - Derived data that depends on multiple signals
108
199
  - Async operations that need automatic cancellation
109
200
  - Cross-cutting concerns that multiple components need
110
201
 
111
202
  ```typescript
112
- const expensiveCalc = createComputed(() => {
203
+ const expensiveCalc = new Memo(() => {
113
204
  return heavyComputation(data1.get(), data2.get()) // Memoized
114
205
  })
115
206
 
116
- const userData = createComputed(async (prev, abort) => {
207
+ const userData = new Task(async (prev, abort) => {
117
208
  const id = userId.get()
118
209
  if (!id) return prev // Keep previous data if no ID
119
210
  const response = await fetch(`/users/${id}`, { signal: abort })
120
211
  return response.json()
121
212
  })
122
213
 
123
- // Reducer-like pattern for state machines
124
- const gameState = createComputed((currentState) => {
214
+ // Reducer pattern for state machines
215
+ const gameState = new Memo(prev => {
125
216
  const action = playerAction.get()
126
- switch (currentState) {
217
+ switch (prev) {
127
218
  case 'menu':
128
219
  return action === 'start' ? 'playing' : 'menu'
129
220
  case 'playing':
@@ -138,7 +229,7 @@ const gameState = createComputed((currentState) => {
138
229
  }, 'menu') // Initial state
139
230
 
140
231
  // Accumulating values over time
141
- const runningTotal = createComputed((previous) => {
232
+ const runningTotal = new Memo(prev => {
142
233
  const newValue = currentValue.get()
143
234
  return previous + newValue
144
235
  }, 0) // Start with 0
@@ -155,13 +246,13 @@ The library provides several layers of error handling:
155
246
 
156
247
  ```typescript
157
248
  // Robust async data fetching with error handling
158
- const apiData = createComputed(async (abort) => {
249
+ const apiData = new Task(async (prev, abort) => {
159
250
  try {
160
251
  const response = await fetch('/api/data', { signal: abort })
161
252
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
162
253
  return response.json()
163
254
  } catch (error) {
164
- if (abort.aborted) return UNSET // Cancelled, not an error
255
+ if (abort.aborted) return prev // Cancelled, not an error
165
256
  throw error // Real error
166
257
  }
167
258
  })
@@ -178,7 +269,7 @@ createEffect(() => {
178
269
 
179
270
  ### Performance Optimization Techniques
180
271
 
181
- 1. **Batching Updates**: Use `batch()` for multiple synchronous updates
272
+ 1. **Batching Updates**: Use `batchSignalWrites()` for multiple synchronous updates
182
273
  2. **Selective Dependencies**: Structure computed signals to minimize dependencies
183
274
  3. **Cleanup Management**: Properly dispose of effects to prevent memory leaks
184
275
  4. **Shallow vs Deep Equality**: Use appropriate comparison strategies
@@ -192,20 +283,20 @@ const user = createStore<{ id: number, name: string, email: string, age?: number
192
283
  })
193
284
 
194
285
  // Batch multiple updates to prevent intermediate states
195
- batch(() => {
286
+ batchSignalWrites(() => {
196
287
  user.name.set('Alice Doe')
197
288
  user.age.set(30)
198
289
  user.email.set('alice@example.com')
199
290
  }) // Only triggers effects once at the end
200
291
 
201
292
  // Optimize computed dependencies
202
- const userDisplay = createComputed(() => {
203
- const { name, email } = user.get() // Gets entire object
293
+ const userDisplay = new Memo(() => {
294
+ const { name, email } = user.get() // Gets entire object, will rerun also if age changes
204
295
  return `${name} <${email}>`
205
296
  })
206
297
 
207
298
  // Better: more granular dependencies
208
- const userDisplay = createComputed(() => {
299
+ const userDisplay = new Memo(() => {
209
300
  return `${user.name.get()} <${user.email.get()}>` // Only depends on name/email
210
301
  })
211
302
  ```
@@ -244,18 +335,18 @@ The library is framework-agnostic but integrates well with:
244
335
  ### Building Reactive Data Structures
245
336
  ```typescript
246
337
  // Reactive list with computed properties
247
- const todos = createStore<Todo[]>([])
248
- const completedCount = createComputed(() =>
338
+ const todos = new List<Todo[]>([])
339
+ const completedCount = new Memo(() =>
249
340
  todos.get().filter(todo => todo.completed).length
250
341
  )
251
- const remainingCount = createComputed(() =>
342
+ const remainingCount = new Memo(() =>
252
343
  todos.get().length - completedCount.get()
253
344
  )
254
345
  ```
255
346
 
256
347
  ### Reactive State Machines
257
348
  ```typescript
258
- const state = createState<'idle' | 'loading' | 'success' | 'error'>('idle')
349
+ const state = new State<'idle' | 'loading' | 'success' | 'error'>('idle')
259
350
  const canRetry = () => state.get() === 'error'
260
351
  const isLoading = () => state.get() === 'loading'
261
352
  ```
@@ -305,6 +396,61 @@ on('userLogin', (data) => {
305
396
  eventBus.userLogin.set({ userId: 123, timestamp: Date.now() })
306
397
  ```
307
398
 
399
+ ### Reactive Data Processing Pipelines
400
+
401
+ ```typescript
402
+ // Build complex data processing with Collections
403
+ const rawData = new List([
404
+ { id: 1, value: 10, category: 'A' },
405
+ { id: 2, value: 20, category: 'B' },
406
+ { id: 3, value: 15, category: 'A' }
407
+ ])
408
+
409
+ // Multi-step transformation pipeline
410
+ const processedData = rawData
411
+ .deriveCollection(item => ({ ...item, doubled: item.value * 2 }))
412
+ .deriveCollection(item => ({ ...item, category: item.category.toLowerCase() }))
413
+
414
+ const categoryTotals = new Collection(processedData, async (item, abort) => {
415
+ // Simulate async processing
416
+ await new Promise(resolve => setTimeout(resolve, 100))
417
+ return { category: item.category, contribution: item.doubled }
418
+ })
419
+
420
+ // Aggregation can be done with computed signals
421
+ const totals = new Memo(() => {
422
+ const items = categoryTotals.get()
423
+ return items.reduce((acc, item) => {
424
+ acc[item.category] = (acc[item.category] || 0) + item.contribution
425
+ return acc
426
+ }, {} as Record<string, number>)
427
+ })
428
+ ```
429
+
430
+ ### Stable Keys for Persistent Item Identity
431
+ ```typescript
432
+ // Managing a list of items where order matters but identity persists
433
+ const playlist = new List([
434
+ { id: 'track1', title: 'Song A', duration: 180 },
435
+ { id: 'track2', title: 'Song B', duration: 210 }
436
+ ], track => track.id)
437
+
438
+ // Get persistent reference to a specific track
439
+ const firstTrackSignal = playlist.byKey('track1')
440
+ const firstTrack = firstTrackSignal?.get()
441
+
442
+ // Reorder playlist while maintaining references
443
+ playlist.sort((a, b) => a.title.localeCompare(b.title))
444
+ // firstTrackSignal still points to the same track!
445
+
446
+ // Insert new track at specific position
447
+ playlist.splice(1, 0, { id: 'track3', title: 'Song C', duration: 195 })
448
+
449
+ // Find current position of a track by its stable key
450
+ const track1Position = playlist.indexOfKey('track1')
451
+ const keyAtPosition2 = playlist.keyAt(2)
452
+ ```
453
+
308
454
  **Component ownership principle**: The component that emits events should own and initialize the event store. This creates clear boundaries and prevents coupling issues.
309
455
 
310
456
  **Why this pattern?**: By having the owning component (Component B) initialize all known events with `UNSET`, we get:
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 - 2025 Zeix AG
3
+ Copyright (c) 2024 - 2026 Zeix AG
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal