@zeix/cause-effect 0.17.2 → 0.17.3

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 (50) hide show
  1. package/.ai-context.md +11 -5
  2. package/.github/copilot-instructions.md +1 -1
  3. package/.zed/settings.json +3 -0
  4. package/CLAUDE.md +18 -79
  5. package/README.md +23 -37
  6. package/archive/benchmark.ts +0 -5
  7. package/archive/collection.ts +5 -62
  8. package/archive/composite.ts +85 -0
  9. package/archive/computed.ts +17 -20
  10. package/archive/list.ts +6 -67
  11. package/archive/memo.ts +13 -14
  12. package/archive/store.ts +7 -66
  13. package/archive/task.ts +18 -20
  14. package/index.dev.js +438 -614
  15. package/index.js +1 -1
  16. package/index.ts +8 -19
  17. package/package.json +6 -6
  18. package/src/classes/collection.ts +59 -112
  19. package/src/classes/computed.ts +146 -189
  20. package/src/classes/list.ts +138 -105
  21. package/src/classes/ref.ts +16 -42
  22. package/src/classes/state.ts +16 -45
  23. package/src/classes/store.ts +107 -72
  24. package/src/effect.ts +9 -12
  25. package/src/errors.ts +12 -8
  26. package/src/signal.ts +3 -1
  27. package/src/system.ts +136 -154
  28. package/test/batch.test.ts +4 -11
  29. package/test/benchmark.test.ts +4 -2
  30. package/test/collection.test.ts +46 -306
  31. package/test/computed.test.ts +205 -223
  32. package/test/list.test.ts +35 -303
  33. package/test/ref.test.ts +38 -66
  34. package/test/state.test.ts +6 -12
  35. package/test/store.test.ts +37 -489
  36. package/test/util/dependency-graph.ts +2 -2
  37. package/tsconfig.build.json +11 -0
  38. package/tsconfig.json +5 -7
  39. package/types/index.d.ts +2 -2
  40. package/types/src/classes/collection.d.ts +4 -6
  41. package/types/src/classes/computed.d.ts +17 -37
  42. package/types/src/classes/list.d.ts +8 -6
  43. package/types/src/classes/ref.d.ts +7 -20
  44. package/types/src/classes/state.d.ts +5 -17
  45. package/types/src/classes/store.d.ts +12 -11
  46. package/types/src/errors.d.ts +2 -4
  47. package/types/src/signal.d.ts +3 -2
  48. package/types/src/system.d.ts +41 -44
  49. package/src/classes/composite.ts +0 -171
  50. package/types/src/classes/composite.d.ts +0 -15
package/.ai-context.md CHANGED
@@ -280,12 +280,18 @@ const finalResults = processedItems.deriveCollection(item =>
280
280
  elementRef.notify() // Notify when DOM element changes externally
281
281
  cacheRef.notify() // Notify when Map/Set changes externally
282
282
 
283
- // Resource management with watch hooks
284
- signal.on('watch', () => {
285
- console.log('Setting up resource...')
286
- const resource = createResource()
287
- return () => resource.cleanup() // Called when no effects watch this signal
283
+ // Resource management with watch callbacks
284
+ const endpoint = new State('https://api.example.com', {
285
+ watched: () => {
286
+ console.log('Setting up API client...')
287
+ const resource = createResource(endpoint.get())
288
+ },
289
+ unwatched: () => {
290
+ console.log('Cleaning up API client...')
291
+ resource.cleanup()
292
+ }
288
293
  })
294
+
289
295
  ```
290
296
 
291
297
  ## Build and Development
@@ -175,7 +175,7 @@ const activeUserSummaries = users
175
175
 
176
176
  ## Resource Management
177
177
 
178
- All signals support `.on('watch', callback)` for lazy resource allocation. Resources are only created when signals are accessed by effects and automatically cleaned up when no longer watched.
178
+ All signals support `watched` and `unwatched` callbacks in signal configuration (optional second parameter) for lazy resource allocation. Resources are only created when signals are accessed by effects and automatically cleaned up when no longer watched.
179
179
 
180
180
  ## When suggesting code:
181
181
  1. Follow the established patterns for signal creation and usage
@@ -0,0 +1,3 @@
1
+ {
2
+ "project_name": "Cause & Effect"
3
+ }
package/CLAUDE.md CHANGED
@@ -78,19 +78,14 @@ Key patterns:
78
78
 
79
79
  **Store signals** (`createStore`): Transform objects into reactive data structures
80
80
  - Each property becomes its own signal via Proxy
81
- - Built on `Composite` class for signal management
81
+ - Lazy signal creation and automatic cleanup
82
82
  - Dynamic property addition/removal with proper reactivity
83
83
 
84
84
  **List signals** (`new List`): Arrays with stable keys and reactive items
85
- - Maintains stable keys that survive sorting and reordering
85
+ - Maintains stable keys that survive sorting and splicing
86
86
  - Built on `Composite` class for consistent signal management
87
87
  - Provides `byKey()`, `keyAt()`, `indexOfKey()` for key-based access
88
88
 
89
- **Composite Architecture**: Shared foundation for Store and List
90
- - `Map<string, Signal>` for property/item signals
91
- - Hook system for granular add/change/remove notifications
92
- - Lazy signal creation and automatic cleanup
93
-
94
89
  ### Computed Signal Memoization Strategy
95
90
 
96
91
  Computed signals implement smart memoization:
@@ -100,84 +95,29 @@ Computed signals implement smart memoization:
100
95
  - **Error Handling**: Preserves error states and prevents cascade failures
101
96
  - **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
102
97
 
103
- ## Resource Management with Watch Hooks
98
+ ## Resource Management with Watch Callbacks
104
99
 
105
- All signals support the `watch` hook for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
100
+ All signals support the `watched` and `unwatched` callbacks for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
106
101
 
107
102
  ```typescript
108
- // Basic watch hook pattern
109
- const config = new State({ apiUrl: 'https://api.example.com' })
110
-
111
- config.on('watch', () => {
112
- console.log('Setting up API client...')
113
- const client = new ApiClient(config.get().apiUrl)
114
-
115
- return () => {
103
+ // Basic watch callbacks pattern
104
+ const config = new State({ apiUrl: 'https://api.example.com' }, {
105
+ watched: () => {
106
+ console.log('Setting up API client...')
107
+ const client = new ApiClient(config.get().apiUrl)
108
+ },
109
+ unwatched: () => {
116
110
  console.log('Cleaning up API client...')
117
111
  client.disconnect()
118
112
  }
119
113
  })
120
114
 
121
115
  // Resource is only created when effect runs
122
- createEffect(() => {
123
- console.log('API URL:', config.get().apiUrl) // Triggers hook
124
- })
125
- ```
126
-
127
- **Store Watch Hooks**: Monitor entire store or nested properties
128
-
129
- ```typescript
130
- const database = createStore({ host: 'localhost', port: 5432 })
131
-
132
- // Watch entire store - triggers when any property accessed
133
- database.on('watch', () => {
134
- console.log('Database connection needed')
135
- const connection = connect(database.host.get(), database.port.get())
136
- return () => connection.close()
137
- })
138
-
139
- // Watch specific property
140
- database.host.on('watch', () => {
141
- console.log('Host property being watched')
142
- return () => console.log('Host watching stopped')
143
- })
144
- ```
145
-
146
- **List Watch Hooks**: Two-tier system for collection and item resources
147
-
148
- ```typescript
149
- const items = new List(['apple', 'banana'])
150
-
151
- // List-level resource (entire collection)
152
- items.on('watch', () => {
153
- console.log('List observer started')
154
- return () => console.log('List observer stopped')
155
- })
156
-
157
- // Item-level resource (individual items)
158
- const firstItem = items.at(0)
159
- firstItem.on('watch', () => {
160
- console.log('First item being watched')
161
- return () => console.log('First item watch stopped')
162
- })
163
- ```
164
-
165
- **Collection Watch Hooks**: Propagate to source List items
166
-
167
- ```typescript
168
- const numbers = new List([1, 2, 3])
169
- const doubled = numbers.deriveCollection(x => x * 2)
170
-
171
- // Set up source item hook
172
- numbers.at(0).on('watch', () => {
173
- console.log('Source item accessed through collection')
174
- return () => console.log('Source item no longer watched')
116
+ const cleanup = createEffect(() => {
117
+ console.log('API URL:', config.get().apiUrl) // Triggers watched callback
175
118
  })
176
119
 
177
- // Accessing collection item triggers source item hook
178
- createEffect(() => {
179
- const value = doubled.at(0).get() // Triggers source item hook
180
- })
120
+ cleanup() // Triggers unwatched callback
181
121
  ```
182
122
 
183
123
  **Practical Use Cases**:
@@ -187,11 +127,10 @@ createEffect(() => {
187
127
  - External subscriptions (WebSocket, Server-Sent Events)
188
128
  - Database connections tied to data access patterns
189
129
 
190
- **Hook Lifecycle**:
191
- 1. First effect accesses signal → `watch` hook callback executed
192
- 2. Hook callback can return cleanup function
193
- 3. Last effect stops watchingcleanup function called
194
- 4. New effect accesses signal → hook callback executed again
130
+ **Watch Lifecycle**:
131
+ 1. First effect accesses signal → `watched` callback executed
132
+ 3. Last effect stops watching `unwatched` callback executed
133
+ 4. New effect accesses signal`watched` callback executed again
195
134
 
196
135
  This pattern enables **lazy resource allocation** - resources are only consumed when actually needed and automatically freed when no longer used.
197
136
 
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.17.2
3
+ Version 0.17.3
4
4
 
5
- **Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
5
+ **Cause & Effect** is a tiny (~5kB gzipped), dependency-free reactive state management library for JavaScript. It uses fine-grained signals so derived values and side effects update automatically when their dependencies change.
6
6
 
7
7
  ## What is Cause & Effect?
8
8
 
@@ -159,36 +159,23 @@ user.preferences.theme.set('light')
159
159
  createEffect(() => console.log('User:', user.get()))
160
160
  ```
161
161
 
162
- Dynamic properties using the `add()` and `remove()` methods:
162
+ Iterator for keys using reactive `.keys()` method to observe structural changes:
163
163
 
164
164
  ```js
165
- const settings = createStore({ autoSave: true })
166
-
167
- settings.add('timeout', 5000)
168
- settings.remove('timeout')
165
+ for (const key of user.keys()) {
166
+ console.log(key)
167
+ }
169
168
  ```
170
169
 
171
- Subscribe to hooks using the `.on()` method:
170
+ Access items by key using `.byKey()` or via direct property access like `user.name` (enabled by the Proxy `createStore()` returns).
172
171
 
173
- ```js
174
- const user = createStore({ name: 'Alice', age: 30 })
175
-
176
- const offChange = user.on('change', changed => console.log(changed))
177
- const offAdd = user.on('add', added => console.log(added))
178
- const offRemove = user.on('remove', removed => console.log(removed))
179
-
180
- // These will trigger the respective hooks:
181
- user.add('email', 'alice@example.com') // Logs: "Added properties: ['email']"
182
- user.age.set(31) // Logs: "Changed properties: ['age']"
183
- user.remove('email') // Logs: "Removed properties: ['email']"
184
- ```
185
-
186
- To unregister hooks, call the returned cleanup functions:
172
+ Dynamic properties using the `.add()` and `.remove()` methods:
187
173
 
188
174
  ```js
189
- offAdd() // Stop listening to add hook
190
- offChange() // Stop listening to change hook
191
- offRemove() // Stop listening to remove hook
175
+ const settings = createStore({ autoSave: true })
176
+
177
+ settings.add('timeout', 5000)
178
+ settings.remove('timeout')
192
179
  ```
193
180
 
194
181
  ### List
@@ -207,6 +194,8 @@ items.splice(1, 1, 'orange')
207
194
  items.sort()
208
195
  ```
209
196
 
197
+ Access items by key using `.byKey()` or by index using `.at()`. `.indexOfKey()` returns the current index of an item in the list, while `.keyAt()` returns the key of an item at a given position.
198
+
210
199
  Keys are stable across reordering:
211
200
 
212
201
  ```js
@@ -218,7 +207,7 @@ console.log(items.byKey(key)) // 'orange'
218
207
  console.log(items.indexOfKey(key)) // current index
219
208
  ```
220
209
 
221
- Lists have `.add()`, `.remove()` and `.on()` methods like stores. In addition, they have `.sort()` and `.splice()` methods. But unlike stores, deeply nested properties in items are not converted to individual signals.
210
+ Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionally, they have `.sort()`, `.splice()`, and a reactive `.length` property. But unlike stores, deeply nested properties in items are not converted to individual signals. Lists have no Proxy layer and don't support direct property access like `items[0].name`.
222
211
 
223
212
  ### Collection
224
213
 
@@ -399,22 +388,19 @@ cleanup() // Logs: 'Cleanup' and unsubscribes from signal `user`
399
388
  user.set({ name: 'Bob', age: 28 }) // Won't trigger the effect anymore
400
389
  ```
401
390
 
402
- ### Resource Management with Hooks
391
+ ### Resource Management with Watch Callbacks
403
392
 
404
- All signals support the `watch` hook for lazy resource management. Resources are only allocated when the signal is first accessed by an effect, and automatically cleaned up when no effects are watching:
393
+ All signals support a options object with `watched` and `unwatched` callbacks for lazy resource management. Resources are only allocated when the signal is first accessed by an effect, and automatically cleaned up when no effects are watching:
405
394
 
406
395
  ```js
407
396
  import { State, createEffect } from '@zeix/cause-effect'
408
397
 
409
- const config = new State({ apiUrl: 'https://api.example.com' })
410
-
411
- // Set up lazy resource management
412
- config.on('watch', () => {
413
- console.log('Setting up API client...')
414
- const client = new ApiClient(config.get().apiUrl)
415
-
416
- // Return cleanup function
417
- return () => {
398
+ const config = new State({ apiUrl: 'https://api.example.com' }, {
399
+ watched: () => {
400
+ console.log('Setting up API client...')
401
+ const client = new ApiClient(config.get().apiUrl)
402
+ },
403
+ unwatched: () => {
418
404
  console.log('Cleaning up API client...')
419
405
  client.disconnect()
420
406
  }
@@ -159,7 +159,6 @@ const benchmarkFactory = async () => {
159
159
  const memoryStores = await measureMemory('Factory Memory Usage', () => {
160
160
  const tempStores = []
161
161
  for (let i = 0; i < ITERATIONS; i++)
162
- // @ts-expect-error ignore
163
162
  tempStores.push(createFactoryStore({ ...testData, id: i }))
164
163
  return tempStores
165
164
  })
@@ -226,7 +225,6 @@ const benchmarkFactoryList = async () => {
226
225
  const tempLists = []
227
226
  for (let i = 0; i < ITERATIONS; i++) {
228
227
  tempLists.push(
229
- // @ts-expect-error ignore
230
228
  createFactoryList([
231
229
  ...testListData.map(item => ({
232
230
  ...item,
@@ -305,7 +303,6 @@ const benchmarkDirectClassList = async () => {
305
303
  const tempLists = []
306
304
  for (let i = 0; i < ITERATIONS; i++) {
307
305
  tempLists.push(
308
- // @ts-expect-error ignore
309
306
  new List([
310
307
  ...testListData.map(item => ({
311
308
  ...item,
@@ -375,7 +372,6 @@ const benchmarkClass = async () => {
375
372
  const memoryStores = await measureMemory('Class Memory Usage', () => {
376
373
  const tempStores = []
377
374
  for (let i = 0; i < ITERATIONS; i++)
378
- // @ts-expect-error ignore
379
375
  tempStores.push(createClassStore({ ...testData, id: i }))
380
376
  return tempStores
381
377
  })
@@ -436,7 +432,6 @@ const benchmarkDirectClass = async () => {
436
432
  () => {
437
433
  const tempStores = []
438
434
  for (let i = 0; i < ITERATIONS; i++)
439
- // @ts-expect-error ignore
440
435
  tempStores.push(new BaseStore({ ...testData, id: i }))
441
436
  return tempStores
442
437
  },
@@ -3,17 +3,11 @@ import { match } from '../src/match'
3
3
  import { resolve } from '../src/resolve'
4
4
  import type { Signal } from '../src/signal'
5
5
  import {
6
- type Cleanup,
7
6
  createWatcher,
8
- triggerHook,
9
- type HookCallback,
10
- type HookCallbacks,
11
- type Hook,
12
7
  notifyWatchers,
13
8
  subscribeActiveWatcher,
14
- trackSignalReads,
15
- type Watcher,
16
9
  UNSET,
10
+ type Watcher,
17
11
  } from '../src/system'
18
12
  import { isAsyncFunction, isObjectOfType, isSymbol } from '../src/util'
19
13
  import { type Computed, createComputed } from './computed'
@@ -40,7 +34,6 @@ type Collection<T extends {}> = {
40
34
  get(): T[]
41
35
  keyAt(index: number): string | undefined
42
36
  indexOfKey(key: string): number
43
- on(type: Hook, callback: HookCallback): Cleanup
44
37
  sort(compareFn?: (a: T, b: T) => number): void
45
38
  }
46
39
 
@@ -67,7 +60,6 @@ const createCollection = <T extends {}, O extends {}>(
67
60
  callback: CollectionCallback<T, O>,
68
61
  ): Collection<T> => {
69
62
  const watchers = new Set<Watcher>()
70
- const hookCallbacks: HookCallbacks = {}
71
63
  const signals = new Map<string, Signal<T>>()
72
64
  const signalWatchers = new Map<string, Watcher>()
73
65
 
@@ -114,64 +106,23 @@ const createCollection = <T extends {}, O extends {}>(
114
106
  // Set internal states
115
107
  signals.set(key, signal)
116
108
  if (!order.includes(key)) order.push(key)
117
- const watcher = createWatcher(() =>
118
- trackSignalReads(watcher, () => {
109
+ const watcher = createWatcher(
110
+ () => {
119
111
  signal.get() // Subscribe to the signal
120
- triggerHook(hookCallbacks.change, [key])
121
- }),
112
+ },
113
+ () => {},
122
114
  )
123
115
  watcher()
124
116
  signalWatchers.set(key, watcher)
125
117
  return true
126
118
  }
127
119
 
128
- // Remove nested signal and effect
129
- const removeProperty = (key: string) => {
130
- // Remove signal for key
131
- const ok = signals.delete(key)
132
- if (!ok) return
133
-
134
- // Clean up internal states
135
- const index = order.indexOf(key)
136
- if (index >= 0) order.splice(index, 1)
137
- const watcher = signalWatchers.get(key)
138
- if (watcher) {
139
- watcher.stop()
140
- signalWatchers.delete(key)
141
- }
142
- }
143
-
144
120
  // Initialize properties
145
121
  for (let i = 0; i < origin.length; i++) {
146
122
  const key = origin.keyAt(i)
147
123
  if (!key) continue
148
124
  addProperty(key)
149
125
  }
150
- origin.on('add', additions => {
151
- if (!additions?.length) return
152
- for (const key of additions) {
153
- if (!signals.has(key)) addProperty(key)
154
- }
155
- notifyWatchers(watchers)
156
- triggerHook(hookCallbacks.add, additions)
157
- })
158
- origin.on('remove', removals => {
159
- if (!removals?.length) return
160
- for (const key of Object.keys(removals)) {
161
- if (!signals.has(key)) continue
162
- removeProperty(key)
163
- }
164
- order = order.filter(() => true) // Compact array
165
- notifyWatchers(watchers)
166
- triggerHook(hookCallbacks.remove, removals)
167
- })
168
- origin.on('sort', newOrder => {
169
- if (newOrder) {
170
- order = [...newOrder]
171
- notifyWatchers(watchers)
172
- triggerHook(hookCallbacks.sort, newOrder)
173
- }
174
- })
175
126
 
176
127
  // Get signal by key or index
177
128
  const getSignal = (prop: string): Signal<T> | undefined => {
@@ -247,14 +198,6 @@ const createCollection = <T extends {}, O extends {}>(
247
198
  order = entries.map(([_, key]) => key)
248
199
 
249
200
  notifyWatchers(watchers)
250
- triggerHook(hookCallbacks.sort, order)
251
- },
252
- },
253
- on: {
254
- value: (type: Hook, callback: HookCallback): Cleanup => {
255
- hookCallbacks[type] ||= new Set()
256
- hookCallbacks[type].add(callback)
257
- return () => hookCallbacks[type]?.delete(callback)
258
201
  },
259
202
  },
260
203
  length: {
@@ -0,0 +1,85 @@
1
+ import type { DiffResult, UnknownRecord } from '../src/diff'
2
+ import { guardMutableSignal } from '../src/errors'
3
+ import type { Signal } from '../src/signal'
4
+ import { batch } from '../src/system'
5
+
6
+ /* === Class Definitions === */
7
+
8
+ class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
9
+ signals = new Map<string, S>()
10
+ #validate: <K extends keyof T & string>(
11
+ key: K,
12
+ value: unknown,
13
+ ) => value is T[K] & {}
14
+ #create: <V extends T[keyof T] & {}>(value: V) => S
15
+
16
+ constructor(
17
+ values: T,
18
+ validate: <K extends keyof T & string>(
19
+ key: K,
20
+ value: unknown,
21
+ ) => value is T[K] & {},
22
+ create: <V extends T[keyof T] & {}>(value: V) => S,
23
+ ) {
24
+ this.#validate = validate
25
+ this.#create = create
26
+ this.change({
27
+ add: values,
28
+ change: {},
29
+ remove: {},
30
+ changed: true,
31
+ })
32
+ }
33
+
34
+ add<K extends keyof T & string>(key: K, value: T[K]): boolean {
35
+ if (!this.#validate(key, value)) return false
36
+
37
+ this.signals.set(key, this.#create(value))
38
+ return true
39
+ }
40
+
41
+ remove<K extends keyof T & string>(key: K): boolean {
42
+ return this.signals.delete(key)
43
+ }
44
+
45
+ change(changes: DiffResult): boolean {
46
+ // Additions
47
+ if (Object.keys(changes.add).length) {
48
+ for (const key in changes.add)
49
+ this.add(
50
+ key as Extract<keyof T, string>,
51
+ changes.add[key] as T[Extract<keyof T, string>] & {},
52
+ )
53
+ }
54
+
55
+ // Changes
56
+ if (Object.keys(changes.change).length) {
57
+ batch(() => {
58
+ for (const key in changes.change) {
59
+ const value = changes.change[key]
60
+ if (!this.#validate(key as keyof T & string, value))
61
+ continue
62
+
63
+ const signal = this.signals.get(key)
64
+ if (guardMutableSignal(`list item "${key}"`, value, signal))
65
+ signal.set(value)
66
+ }
67
+ })
68
+ }
69
+
70
+ // Removals
71
+ if (Object.keys(changes.remove).length) {
72
+ for (const key in changes.remove)
73
+ this.remove(key as keyof T & string)
74
+ }
75
+
76
+ return changes.changed
77
+ }
78
+
79
+ clear(): boolean {
80
+ this.signals.clear()
81
+ return true
82
+ }
83
+ }
84
+
85
+ export { Composite }
@@ -7,11 +7,9 @@ import {
7
7
  } from '../src/errors'
8
8
  import {
9
9
  createWatcher,
10
- flushPendingReactions,
11
- HOOK_CLEANUP,
10
+ flush,
12
11
  notifyWatchers,
13
12
  subscribeActiveWatcher,
14
- trackSignalReads,
15
13
  UNSET,
16
14
  type Watcher,
17
15
  } from '../src/system'
@@ -97,19 +95,14 @@ const createComputed = <T extends {}>(
97
95
  }
98
96
 
99
97
  // Own watcher: called when notified from sources (push)
100
- const watcher = createWatcher(() => {
101
- dirty = true
102
- controller?.abort()
103
- if (watchers.size) notifyWatchers(watchers)
104
- else watcher.stop()
105
- })
106
- watcher.on(HOOK_CLEANUP, () => {
107
- controller?.abort()
108
- })
109
-
110
- // Called when requested by dependencies (pull)
111
- const compute = () =>
112
- trackSignalReads(watcher, () => {
98
+ const watcher = createWatcher(
99
+ () => {
100
+ dirty = true
101
+ controller?.abort()
102
+ if (watchers.size) notifyWatchers(watchers)
103
+ else watcher.stop()
104
+ },
105
+ () => {
113
106
  if (computing) throw new CircularDependencyError('computed')
114
107
  changed = false
115
108
  if (isAsyncFunction(callback)) {
@@ -121,7 +114,7 @@ const createComputed = <T extends {}>(
121
114
  () => {
122
115
  computing = false
123
116
  controller = undefined
124
- compute() // Retry computation with updated state
117
+ watcher.run() // Retry computation with updated state
125
118
  },
126
119
  {
127
120
  once: true,
@@ -144,7 +137,11 @@ const createComputed = <T extends {}>(
144
137
  else if (null == result || UNSET === result) nil()
145
138
  else ok(result)
146
139
  computing = false
147
- })
140
+ },
141
+ )
142
+ watcher.onCleanup(() => {
143
+ controller?.abort()
144
+ })
148
145
 
149
146
  const computed: Record<PropertyKey, unknown> = {}
150
147
  Object.defineProperties(computed, {
@@ -154,8 +151,8 @@ const createComputed = <T extends {}>(
154
151
  get: {
155
152
  value: (): T => {
156
153
  subscribeActiveWatcher(watchers)
157
- flushPendingReactions()
158
- if (dirty) compute()
154
+ flush()
155
+ if (dirty) watcher.run()
159
156
  if (error) throw error
160
157
  return value
161
158
  },