@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.
- package/.ai-context.md +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +19 -19
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -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 +899 -503
- package/index.js +1 -1
- package/index.ts +41 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -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 +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -30
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -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/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 +10 -8
- package/types/src/classes/collection.d.ts +32 -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/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 +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- 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
|
@@ -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
|
-
- **
|
|
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
|
-
│ ├──
|
|
29
|
-
│ ├── state.ts # Mutable state signals (
|
|
30
|
-
│ ├── store.ts # Object stores with reactive properties
|
|
31
|
-
│ ├──
|
|
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 =
|
|
50
|
-
const name =
|
|
51
|
-
const actions =
|
|
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 =
|
|
66
|
+
const doubled = new Memo(() => count.get() * 2)
|
|
58
67
|
|
|
59
|
-
// Computed with reducer
|
|
60
|
-
const counter =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
148
|
-
const error =
|
|
164
|
+
const loading = new State(false)
|
|
165
|
+
const error = new State('') // Empty string means no error
|
|
149
166
|
|
|
150
|
-
//
|
|
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
|
|
160
|
-
const appState =
|
|
197
|
+
// Reducer pattern for state machines
|
|
198
|
+
const appState = new Memo((prev) => {
|
|
161
199
|
const event = events.get()
|
|
162
|
-
switch (
|
|
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 =
|
|
218
|
+
const userData = new Task(async (prev, abort) => {
|
|
181
219
|
const id = userId.get()
|
|
182
|
-
if (!id) return
|
|
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:
|
|
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
|
-
-
|
|
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 (`
|
|
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
|
-
- **
|
|
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., `
|
|
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 =
|
|
73
|
-
const name =
|
|
74
|
-
const actions =
|
|
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 =
|
|
88
|
+
const doubled = new Memo(() => count.get() * 2)
|
|
81
89
|
|
|
82
|
-
// Computed with reducer
|
|
83
|
-
const counter =
|
|
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 =
|
|
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
|
|
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 (`
|
|
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 =
|
|
86
|
-
const theme =
|
|
87
|
-
const user =
|
|
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
|
-
**
|
|
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 =
|
|
203
|
+
const expensiveCalc = new Memo(() => {
|
|
113
204
|
return heavyComputation(data1.get(), data2.get()) // Memoized
|
|
114
205
|
})
|
|
115
206
|
|
|
116
|
-
const userData =
|
|
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
|
|
124
|
-
const gameState =
|
|
214
|
+
// Reducer pattern for state machines
|
|
215
|
+
const gameState = new Memo(prev => {
|
|
125
216
|
const action = playerAction.get()
|
|
126
|
-
switch (
|
|
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 =
|
|
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 =
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
248
|
-
const completedCount =
|
|
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 =
|
|
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 =
|
|
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