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