@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.
- package/.ai-context.md +11 -5
- package/.github/copilot-instructions.md +1 -1
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +18 -79
- package/README.md +23 -37
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +5 -62
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +17 -20
- package/archive/list.ts +6 -67
- package/archive/memo.ts +13 -14
- package/archive/store.ts +7 -66
- package/archive/task.ts +18 -20
- package/index.dev.js +438 -614
- package/index.js +1 -1
- package/index.ts +8 -19
- package/package.json +6 -6
- package/src/classes/collection.ts +59 -112
- package/src/classes/computed.ts +146 -189
- package/src/classes/list.ts +138 -105
- package/src/classes/ref.ts +16 -42
- package/src/classes/state.ts +16 -45
- package/src/classes/store.ts +107 -72
- package/src/effect.ts +9 -12
- package/src/errors.ts +12 -8
- package/src/signal.ts +3 -1
- package/src/system.ts +136 -154
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +46 -306
- package/test/computed.test.ts +205 -223
- package/test/list.test.ts +35 -303
- package/test/ref.test.ts +38 -66
- package/test/state.test.ts +6 -12
- package/test/store.test.ts +37 -489
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +2 -2
- package/types/src/classes/collection.d.ts +4 -6
- package/types/src/classes/computed.d.ts +17 -37
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +7 -20
- package/types/src/classes/state.d.ts +5 -17
- package/types/src/classes/store.d.ts +12 -11
- package/types/src/errors.d.ts +2 -4
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +41 -44
- package/src/classes/composite.ts +0 -171
- package/types/src/classes/composite.d.ts +0 -15
package/src/classes/computed.ts
CHANGED
|
@@ -2,23 +2,18 @@ import { isEqual } from '../diff'
|
|
|
2
2
|
import {
|
|
3
3
|
CircularDependencyError,
|
|
4
4
|
createError,
|
|
5
|
-
InvalidHookError,
|
|
6
5
|
validateCallback,
|
|
7
6
|
validateSignalValue,
|
|
8
7
|
} from '../errors'
|
|
9
8
|
import {
|
|
10
|
-
type Cleanup,
|
|
11
9
|
createWatcher,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
subscribeActiveWatcher,
|
|
18
|
-
trackSignalReads,
|
|
10
|
+
flush,
|
|
11
|
+
notifyOf,
|
|
12
|
+
registerWatchCallbacks,
|
|
13
|
+
type SignalOptions,
|
|
14
|
+
subscribeTo,
|
|
19
15
|
UNSET,
|
|
20
16
|
type Watcher,
|
|
21
|
-
type WatchHook,
|
|
22
17
|
} from '../system'
|
|
23
18
|
import {
|
|
24
19
|
isAbortError,
|
|
@@ -34,6 +29,10 @@ type Computed<T extends {}> = {
|
|
|
34
29
|
get(): T
|
|
35
30
|
}
|
|
36
31
|
|
|
32
|
+
type ComputedOptions<T extends {}> = SignalOptions<T> & {
|
|
33
|
+
initialValue?: T
|
|
34
|
+
}
|
|
35
|
+
|
|
37
36
|
type MemoCallback<T extends {} & { then?: undefined }> = (oldValue: T) => T
|
|
38
37
|
|
|
39
38
|
type TaskCallback<T extends {} & { then?: undefined }> = (
|
|
@@ -51,65 +50,38 @@ const TYPE_COMPUTED = 'Computed' as const
|
|
|
51
50
|
* Create a new memoized signal for a synchronous function.
|
|
52
51
|
*
|
|
53
52
|
* @since 0.17.0
|
|
53
|
+
* @param {MemoCallback<T>} callback - Callback function to compute the memoized value
|
|
54
|
+
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
55
|
+
* @throws {InvalidCallbackError} If the callback is not an sync function
|
|
56
|
+
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
54
57
|
*/
|
|
55
58
|
class Memo<T extends {}> {
|
|
56
|
-
#watchers: Set<Watcher> = new Set()
|
|
57
59
|
#callback: MemoCallback<T>
|
|
58
60
|
#value: T
|
|
59
61
|
#error: Error | undefined
|
|
60
62
|
#dirty = true
|
|
61
63
|
#computing = false
|
|
62
64
|
#watcher: Watcher | undefined
|
|
63
|
-
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
* Create a new memoized signal.
|
|
67
|
-
*
|
|
68
|
-
* @param {MemoCallback<T>} callback - Callback function to compute the memoized value
|
|
69
|
-
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
70
|
-
* @throws {InvalidCallbackError} If the callback is not an sync function
|
|
71
|
-
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
72
|
-
*/
|
|
73
|
-
constructor(callback: MemoCallback<T>, initialValue: T = UNSET) {
|
|
66
|
+
constructor(callback: MemoCallback<T>, options?: ComputedOptions<T>) {
|
|
74
67
|
validateCallback(this.constructor.name, callback, isMemoCallback)
|
|
75
|
-
|
|
68
|
+
const initialValue = options?.initialValue ?? UNSET
|
|
69
|
+
validateSignalValue(this.constructor.name, initialValue, options?.guard)
|
|
76
70
|
|
|
77
71
|
this.#callback = callback
|
|
78
72
|
this.#value = initialValue
|
|
73
|
+
if (options?.watched)
|
|
74
|
+
registerWatchCallbacks(this, options.watched, options.unwatched)
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
#getWatcher(): Watcher {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
// Own watcher: called by notifyWatchers() in upstream signals (push)
|
|
79
|
+
this.#watcher ||= createWatcher(
|
|
80
|
+
() => {
|
|
85
81
|
this.#dirty = true
|
|
86
|
-
if (!
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.#watcher = undefined
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
return this.#watcher
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get [Symbol.toStringTag](): 'Computed' {
|
|
96
|
-
return TYPE_COMPUTED
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Return the memoized value after computing it if necessary.
|
|
101
|
-
*
|
|
102
|
-
* @returns {T}
|
|
103
|
-
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
104
|
-
* @throws {Error} If an error occurs during computation
|
|
105
|
-
*/
|
|
106
|
-
get(): T {
|
|
107
|
-
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
108
|
-
flushPendingReactions()
|
|
109
|
-
|
|
110
|
-
if (this.#dirty) {
|
|
111
|
-
const watcher = this.#getWatcher()
|
|
112
|
-
trackSignalReads(watcher, () => {
|
|
82
|
+
if (!notifyOf(this)) this.#watcher?.stop()
|
|
83
|
+
},
|
|
84
|
+
() => {
|
|
113
85
|
if (this.#computing) throw new CircularDependencyError('memo')
|
|
114
86
|
|
|
115
87
|
let result: T
|
|
@@ -135,29 +107,33 @@ class Memo<T extends {}> {
|
|
|
135
107
|
this.#dirty = false
|
|
136
108
|
}
|
|
137
109
|
this.#computing = false
|
|
138
|
-
}
|
|
139
|
-
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
this.#watcher.onCleanup(() => {
|
|
113
|
+
this.#watcher = undefined
|
|
114
|
+
})
|
|
140
115
|
|
|
141
|
-
|
|
142
|
-
|
|
116
|
+
return this.#watcher
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get [Symbol.toStringTag](): 'Computed' {
|
|
120
|
+
return TYPE_COMPUTED
|
|
143
121
|
}
|
|
144
122
|
|
|
145
123
|
/**
|
|
146
|
-
*
|
|
124
|
+
* Return the memoized value after computing it if necessary.
|
|
147
125
|
*
|
|
148
|
-
* @
|
|
149
|
-
* @
|
|
150
|
-
* @
|
|
126
|
+
* @returns {T}
|
|
127
|
+
* @throws {CircularDependencyError} If a circular dependency is detected
|
|
128
|
+
* @throws {Error} If an error occurs during computation
|
|
151
129
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
throw new InvalidHookError(this.constructor.name, type)
|
|
130
|
+
get(): T {
|
|
131
|
+
subscribeTo(this)
|
|
132
|
+
flush()
|
|
133
|
+
|
|
134
|
+
if (this.#dirty) this.#getWatcher().run()
|
|
135
|
+
if (this.#error) throw this.#error
|
|
136
|
+
return this.#value
|
|
161
137
|
}
|
|
162
138
|
}
|
|
163
139
|
|
|
@@ -165,9 +141,12 @@ class Memo<T extends {}> {
|
|
|
165
141
|
* Create a new task signals that memoizes the result of an asynchronous function.
|
|
166
142
|
*
|
|
167
143
|
* @since 0.17.0
|
|
144
|
+
* @param {TaskCallback<T>} callback - The asynchronous function to compute the memoized value
|
|
145
|
+
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
146
|
+
* @throws {InvalidCallbackError} If the callback is not an async function
|
|
147
|
+
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
168
148
|
*/
|
|
169
149
|
class Task<T extends {}> {
|
|
170
|
-
#watchers: Set<Watcher> = new Set()
|
|
171
150
|
#callback: TaskCallback<T>
|
|
172
151
|
#value: T
|
|
173
152
|
#error: Error | undefined
|
|
@@ -176,38 +155,109 @@ class Task<T extends {}> {
|
|
|
176
155
|
#changed = false
|
|
177
156
|
#watcher: Watcher | undefined
|
|
178
157
|
#controller: AbortController | undefined
|
|
179
|
-
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
180
158
|
|
|
181
|
-
|
|
182
|
-
* Create a new task signal for an asynchronous function.
|
|
183
|
-
*
|
|
184
|
-
* @param {TaskCallback<T>} callback - The asynchronous function to compute the memoized value
|
|
185
|
-
* @param {T} [initialValue = UNSET] - Initial value of the signal
|
|
186
|
-
* @throws {InvalidCallbackError} If the callback is not an async function
|
|
187
|
-
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
188
|
-
*/
|
|
189
|
-
constructor(callback: TaskCallback<T>, initialValue: T = UNSET) {
|
|
159
|
+
constructor(callback: TaskCallback<T>, options?: ComputedOptions<T>) {
|
|
190
160
|
validateCallback(this.constructor.name, callback, isTaskCallback)
|
|
191
|
-
|
|
161
|
+
const initialValue = options?.initialValue ?? UNSET
|
|
162
|
+
validateSignalValue(this.constructor.name, initialValue, options?.guard)
|
|
192
163
|
|
|
193
164
|
this.#callback = callback
|
|
194
165
|
this.#value = initialValue
|
|
166
|
+
if (options?.watched)
|
|
167
|
+
registerWatchCallbacks(this, options.watched, options.unwatched)
|
|
195
168
|
}
|
|
196
169
|
|
|
197
170
|
#getWatcher(): Watcher {
|
|
198
171
|
if (!this.#watcher) {
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
this.#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
172
|
+
// Functions to update internal state
|
|
173
|
+
const ok = (v: T): undefined => {
|
|
174
|
+
if (!isEqual(v, this.#value)) {
|
|
175
|
+
this.#value = v
|
|
176
|
+
this.#changed = true
|
|
177
|
+
}
|
|
178
|
+
this.#error = undefined
|
|
179
|
+
this.#dirty = false
|
|
180
|
+
}
|
|
181
|
+
const nil = (): undefined => {
|
|
182
|
+
this.#changed = UNSET !== this.#value
|
|
183
|
+
this.#value = UNSET
|
|
184
|
+
this.#error = undefined
|
|
185
|
+
}
|
|
186
|
+
const err = (e: unknown): undefined => {
|
|
187
|
+
const newError = createError(e)
|
|
188
|
+
this.#changed =
|
|
189
|
+
!this.#error ||
|
|
190
|
+
newError.name !== this.#error.name ||
|
|
191
|
+
newError.message !== this.#error.message
|
|
192
|
+
this.#value = UNSET
|
|
193
|
+
this.#error = newError
|
|
194
|
+
}
|
|
195
|
+
const settle =
|
|
196
|
+
<T>(fn: (arg: T) => void) =>
|
|
197
|
+
(arg: T) => {
|
|
198
|
+
this.#computing = false
|
|
199
|
+
this.#controller = undefined
|
|
200
|
+
fn(arg)
|
|
201
|
+
if (this.#changed && !notifyOf(this)) this.#watcher?.stop()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Own watcher: called by notifyOf() in upstream signals (push)
|
|
205
|
+
this.#watcher = createWatcher(
|
|
206
|
+
() => {
|
|
207
|
+
this.#dirty = true
|
|
208
|
+
this.#controller?.abort()
|
|
209
|
+
if (!notifyOf(this)) this.#watcher?.stop()
|
|
210
|
+
},
|
|
211
|
+
() => {
|
|
212
|
+
if (this.#computing)
|
|
213
|
+
throw new CircularDependencyError('task')
|
|
214
|
+
this.#changed = false
|
|
215
|
+
|
|
216
|
+
// Return current value until promise resolves
|
|
217
|
+
if (this.#controller) return this.#value
|
|
218
|
+
|
|
219
|
+
this.#controller = new AbortController()
|
|
220
|
+
this.#controller.signal.addEventListener(
|
|
221
|
+
'abort',
|
|
222
|
+
() => {
|
|
223
|
+
this.#computing = false
|
|
224
|
+
this.#controller = undefined
|
|
225
|
+
|
|
226
|
+
// Retry computation with updated state
|
|
227
|
+
this.#getWatcher().run()
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
once: true,
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
let result: Promise<T>
|
|
234
|
+
this.#computing = true
|
|
235
|
+
try {
|
|
236
|
+
result = this.#callback(
|
|
237
|
+
this.#value,
|
|
238
|
+
this.#controller.signal,
|
|
239
|
+
)
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (isAbortError(e)) nil()
|
|
242
|
+
else err(e)
|
|
243
|
+
this.#computing = false
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (result instanceof Promise)
|
|
248
|
+
result.then(settle(ok), settle(err))
|
|
249
|
+
else if (null == result || UNSET === result) nil()
|
|
250
|
+
else ok(result)
|
|
251
|
+
this.#computing = false
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
this.#watcher.onCleanup(() => {
|
|
206
255
|
this.#controller?.abort()
|
|
207
256
|
this.#controller = undefined
|
|
208
257
|
this.#watcher = undefined
|
|
209
258
|
})
|
|
210
259
|
}
|
|
260
|
+
|
|
211
261
|
return this.#watcher
|
|
212
262
|
}
|
|
213
263
|
|
|
@@ -223,108 +273,13 @@ class Task<T extends {}> {
|
|
|
223
273
|
* @throws {Error} If an error occurs during computation
|
|
224
274
|
*/
|
|
225
275
|
get(): T {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Functions to update internal state
|
|
230
|
-
const ok = (v: T): undefined => {
|
|
231
|
-
if (!isEqual(v, this.#value)) {
|
|
232
|
-
this.#value = v
|
|
233
|
-
this.#changed = true
|
|
234
|
-
}
|
|
235
|
-
this.#error = undefined
|
|
236
|
-
this.#dirty = false
|
|
237
|
-
}
|
|
238
|
-
const nil = (): undefined => {
|
|
239
|
-
this.#changed = UNSET !== this.#value
|
|
240
|
-
this.#value = UNSET
|
|
241
|
-
this.#error = undefined
|
|
242
|
-
}
|
|
243
|
-
const err = (e: unknown): undefined => {
|
|
244
|
-
const newError = createError(e)
|
|
245
|
-
this.#changed =
|
|
246
|
-
!this.#error ||
|
|
247
|
-
newError.name !== this.#error.name ||
|
|
248
|
-
newError.message !== this.#error.message
|
|
249
|
-
this.#value = UNSET
|
|
250
|
-
this.#error = newError
|
|
251
|
-
}
|
|
252
|
-
const settle =
|
|
253
|
-
<T>(fn: (arg: T) => void) =>
|
|
254
|
-
(arg: T) => {
|
|
255
|
-
this.#computing = false
|
|
256
|
-
this.#controller = undefined
|
|
257
|
-
fn(arg)
|
|
258
|
-
if (this.#changed && !notifyWatchers(this.#watchers))
|
|
259
|
-
this.#watcher?.stop()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const compute = () =>
|
|
263
|
-
trackSignalReads(this.#getWatcher(), () => {
|
|
264
|
-
if (this.#computing) throw new CircularDependencyError('task')
|
|
265
|
-
this.#changed = false
|
|
266
|
-
|
|
267
|
-
// Return current value until promise resolves
|
|
268
|
-
if (this.#controller) return this.#value
|
|
269
|
-
|
|
270
|
-
this.#controller = new AbortController()
|
|
271
|
-
this.#controller.signal.addEventListener(
|
|
272
|
-
'abort',
|
|
273
|
-
() => {
|
|
274
|
-
this.#computing = false
|
|
275
|
-
this.#controller = undefined
|
|
276
|
-
|
|
277
|
-
// Retry computation with updated state
|
|
278
|
-
compute()
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
once: true,
|
|
282
|
-
},
|
|
283
|
-
)
|
|
284
|
-
let result: Promise<T>
|
|
285
|
-
this.#computing = true
|
|
286
|
-
try {
|
|
287
|
-
result = this.#callback(
|
|
288
|
-
this.#value,
|
|
289
|
-
this.#controller.signal,
|
|
290
|
-
)
|
|
291
|
-
} catch (e) {
|
|
292
|
-
if (isAbortError(e)) nil()
|
|
293
|
-
else err(e)
|
|
294
|
-
this.#computing = false
|
|
295
|
-
return
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (result instanceof Promise)
|
|
299
|
-
result.then(settle(ok), settle(err))
|
|
300
|
-
else if (null == result || UNSET === result) nil()
|
|
301
|
-
else ok(result)
|
|
302
|
-
this.#computing = false
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
if (this.#dirty) compute()
|
|
276
|
+
subscribeTo(this)
|
|
277
|
+
flush()
|
|
306
278
|
|
|
279
|
+
if (this.#dirty) this.#getWatcher().run()
|
|
307
280
|
if (this.#error) throw this.#error
|
|
308
281
|
return this.#value
|
|
309
282
|
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Register a callback to be called when HOOK_WATCH is triggered.
|
|
313
|
-
*
|
|
314
|
-
* @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
|
|
315
|
-
* @param {HookCallback} callback - The callback to register
|
|
316
|
-
* @returns {Cleanup} - A function to unregister the callback
|
|
317
|
-
*/
|
|
318
|
-
on(type: WatchHook, callback: HookCallback): Cleanup {
|
|
319
|
-
if (type === HOOK_WATCH) {
|
|
320
|
-
this.#watchHookCallbacks ||= new Set()
|
|
321
|
-
this.#watchHookCallbacks.add(callback)
|
|
322
|
-
return () => {
|
|
323
|
-
this.#watchHookCallbacks?.delete(callback)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
throw new InvalidHookError(this.constructor.name, type)
|
|
327
|
-
}
|
|
328
283
|
}
|
|
329
284
|
|
|
330
285
|
/* === Functions === */
|
|
@@ -334,14 +289,15 @@ class Task<T extends {}> {
|
|
|
334
289
|
*
|
|
335
290
|
* @since 0.9.0
|
|
336
291
|
* @param {MemoCallback<T> | TaskCallback<T>} callback - Computation callback function
|
|
292
|
+
* @param {ComputedOptions<T>} options - Optional configuration
|
|
337
293
|
*/
|
|
338
294
|
const createComputed = <T extends {}>(
|
|
339
295
|
callback: TaskCallback<T> | MemoCallback<T>,
|
|
340
|
-
|
|
296
|
+
options?: ComputedOptions<T>,
|
|
341
297
|
) =>
|
|
342
298
|
isAsyncFunction(callback)
|
|
343
|
-
? new Task(callback as TaskCallback<T>,
|
|
344
|
-
: new Memo(callback as MemoCallback<T>,
|
|
299
|
+
? new Task(callback as TaskCallback<T>, options)
|
|
300
|
+
: new Memo(callback as MemoCallback<T>, options)
|
|
345
301
|
|
|
346
302
|
/**
|
|
347
303
|
* Check if a value is a computed signal
|
|
@@ -387,6 +343,7 @@ export {
|
|
|
387
343
|
Memo,
|
|
388
344
|
Task,
|
|
389
345
|
type Computed,
|
|
346
|
+
type ComputedOptions,
|
|
390
347
|
type MemoCallback,
|
|
391
348
|
type TaskCallback,
|
|
392
349
|
}
|