@zeix/cause-effect 0.17.3 → 0.18.1
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 +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +529 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +79 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { DuplicateKeyError, validateSignalValue } from '../errors'
|
|
2
|
+
import {
|
|
3
|
+
activeSink,
|
|
4
|
+
batch,
|
|
5
|
+
batchDepth,
|
|
6
|
+
type Cleanup,
|
|
7
|
+
FLAG_CLEAN,
|
|
8
|
+
FLAG_DIRTY,
|
|
9
|
+
flush,
|
|
10
|
+
link,
|
|
11
|
+
type MemoNode,
|
|
12
|
+
propagate,
|
|
13
|
+
refresh,
|
|
14
|
+
type SinkNode,
|
|
15
|
+
TYPE_STORE,
|
|
16
|
+
untrack,
|
|
17
|
+
} from '../graph'
|
|
18
|
+
import { isObjectOfType, isRecord } from '../util'
|
|
19
|
+
import {
|
|
20
|
+
createList,
|
|
21
|
+
type DiffResult,
|
|
22
|
+
isEqual,
|
|
23
|
+
type List,
|
|
24
|
+
type UnknownRecord,
|
|
25
|
+
} from './list'
|
|
26
|
+
import { createState, type State } from './state'
|
|
27
|
+
|
|
28
|
+
/* === Types === */
|
|
29
|
+
|
|
30
|
+
type StoreOptions = {
|
|
31
|
+
watched?: () => Cleanup
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type BaseStore<T extends UnknownRecord> = {
|
|
35
|
+
readonly [Symbol.toStringTag]: 'Store'
|
|
36
|
+
readonly [Symbol.isConcatSpreadable]: false
|
|
37
|
+
[Symbol.iterator](): IterableIterator<
|
|
38
|
+
[
|
|
39
|
+
string,
|
|
40
|
+
State<T[keyof T] & {}> | Store<UnknownRecord> | List<unknown & {}>,
|
|
41
|
+
]
|
|
42
|
+
>
|
|
43
|
+
keys(): IterableIterator<string>
|
|
44
|
+
byKey<K extends keyof T & string>(
|
|
45
|
+
key: K,
|
|
46
|
+
): T[K] extends readonly (infer U extends {})[]
|
|
47
|
+
? List<U>
|
|
48
|
+
: T[K] extends UnknownRecord
|
|
49
|
+
? Store<T[K]>
|
|
50
|
+
: T[K] extends unknown & {}
|
|
51
|
+
? State<T[K] & {}>
|
|
52
|
+
: State<T[K] & {}> | undefined
|
|
53
|
+
get(): T
|
|
54
|
+
set(next: T): void
|
|
55
|
+
update(fn: (prev: T) => T): void
|
|
56
|
+
add<K extends keyof T & string>(key: K, value: T[K]): K
|
|
57
|
+
remove(key: string): void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
61
|
+
[K in keyof T]: T[K] extends readonly (infer U extends {})[]
|
|
62
|
+
? List<U>
|
|
63
|
+
: T[K] extends UnknownRecord
|
|
64
|
+
? Store<T[K]>
|
|
65
|
+
: T[K] extends unknown & {}
|
|
66
|
+
? State<T[K] & {}>
|
|
67
|
+
: State<T[K] & {}> | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* === Functions === */
|
|
71
|
+
|
|
72
|
+
/** Diff two records and return granular changes */
|
|
73
|
+
function diffRecords<T extends UnknownRecord>(prev: T, next: T): DiffResult {
|
|
74
|
+
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
75
|
+
const prevValid = isRecord(prev) || Array.isArray(prev)
|
|
76
|
+
const nextValid = isRecord(next) || Array.isArray(next)
|
|
77
|
+
if (!prevValid || !nextValid) {
|
|
78
|
+
// For non-objects or non-plain objects, treat as complete change if different
|
|
79
|
+
const changed = !Object.is(prev, next)
|
|
80
|
+
return {
|
|
81
|
+
changed,
|
|
82
|
+
add: changed && nextValid ? next : {},
|
|
83
|
+
change: {},
|
|
84
|
+
remove: changed && prevValid ? prev : {},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const visited = new WeakSet()
|
|
89
|
+
|
|
90
|
+
const add = {} as UnknownRecord
|
|
91
|
+
const change = {} as UnknownRecord
|
|
92
|
+
const remove = {} as UnknownRecord
|
|
93
|
+
let changed = false
|
|
94
|
+
|
|
95
|
+
const prevKeys = Object.keys(prev)
|
|
96
|
+
const nextKeys = Object.keys(next)
|
|
97
|
+
|
|
98
|
+
// Pass 1: iterate new keys — find additions and changes
|
|
99
|
+
for (const key of nextKeys) {
|
|
100
|
+
if (key in prev) {
|
|
101
|
+
if (!isEqual(prev[key], next[key], visited)) {
|
|
102
|
+
change[key] = next[key]
|
|
103
|
+
changed = true
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
add[key] = next[key]
|
|
107
|
+
changed = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pass 2: iterate old keys — find removals
|
|
112
|
+
for (const key of prevKeys) {
|
|
113
|
+
if (!(key in next)) {
|
|
114
|
+
remove[key] = undefined
|
|
115
|
+
changed = true
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { add, change, remove, changed }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Creates a reactive store with deeply nested reactive properties.
|
|
124
|
+
* Each property becomes its own signal (State for primitives, nested Store for objects, List for arrays).
|
|
125
|
+
* Properties are accessible directly via proxy.
|
|
126
|
+
*
|
|
127
|
+
* @since 0.15.0
|
|
128
|
+
* @param value - Initial object value of the store
|
|
129
|
+
* @param options - Optional configuration for watch lifecycle
|
|
130
|
+
* @returns A Store with reactive properties
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const user = createStore({ name: 'Alice', age: 30 });
|
|
135
|
+
* user.name.set('Bob'); // Only name subscribers react
|
|
136
|
+
* console.log(user.get()); // { name: 'Bob', age: 30 }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function createStore<T extends UnknownRecord>(
|
|
140
|
+
value: T,
|
|
141
|
+
options?: StoreOptions,
|
|
142
|
+
): Store<T> {
|
|
143
|
+
validateSignalValue(TYPE_STORE, value, isRecord)
|
|
144
|
+
|
|
145
|
+
const signals = new Map<
|
|
146
|
+
string,
|
|
147
|
+
State<unknown & {}> | Store<UnknownRecord> | List<unknown & {}>
|
|
148
|
+
>()
|
|
149
|
+
|
|
150
|
+
// --- Internal helpers ---
|
|
151
|
+
|
|
152
|
+
const addSignal = (key: string, val: unknown): void => {
|
|
153
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
154
|
+
if (Array.isArray(val)) signals.set(key, createList(val))
|
|
155
|
+
else if (isRecord(val)) signals.set(key, createStore(val))
|
|
156
|
+
else signals.set(key, createState(val as unknown & {}))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build current value from child signals
|
|
160
|
+
const buildValue = (): T => {
|
|
161
|
+
const record = {} as UnknownRecord
|
|
162
|
+
signals.forEach((signal, key) => {
|
|
163
|
+
record[key] = signal.get()
|
|
164
|
+
})
|
|
165
|
+
return record as T
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Structural tracking node — not a general-purpose Memo.
|
|
169
|
+
// On first get(): refresh() establishes edges from child signals.
|
|
170
|
+
// On subsequent get(): untrack(buildValue) rebuilds without re-linking.
|
|
171
|
+
// Mutation methods (add/remove/set) null out sources to force re-establishment.
|
|
172
|
+
const node: MemoNode<T> = {
|
|
173
|
+
fn: buildValue,
|
|
174
|
+
value,
|
|
175
|
+
flags: FLAG_DIRTY,
|
|
176
|
+
sources: null,
|
|
177
|
+
sourcesTail: null,
|
|
178
|
+
sinks: null,
|
|
179
|
+
sinksTail: null,
|
|
180
|
+
equals: isEqual,
|
|
181
|
+
error: undefined,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const applyChanges = (changes: DiffResult): boolean => {
|
|
185
|
+
let structural = false
|
|
186
|
+
|
|
187
|
+
// Additions
|
|
188
|
+
for (const key in changes.add) {
|
|
189
|
+
addSignal(key, changes.add[key])
|
|
190
|
+
structural = true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Changes
|
|
194
|
+
if (Object.keys(changes.change).length) {
|
|
195
|
+
batch(() => {
|
|
196
|
+
for (const key in changes.change) {
|
|
197
|
+
const val = changes.change[key]
|
|
198
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
199
|
+
const signal = signals.get(key)
|
|
200
|
+
if (signal) {
|
|
201
|
+
// Type changed (e.g. primitive → object or vice versa): replace signal
|
|
202
|
+
if (isRecord(val) !== isStore(signal)) {
|
|
203
|
+
addSignal(key, val)
|
|
204
|
+
structural = true
|
|
205
|
+
} else signal.set(val as never)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Removals
|
|
212
|
+
for (const key in changes.remove) {
|
|
213
|
+
signals.delete(key)
|
|
214
|
+
structural = true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (structural) {
|
|
218
|
+
node.sources = null
|
|
219
|
+
node.sourcesTail = null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return changes.changed
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const watched = options?.watched
|
|
226
|
+
const subscribe = watched
|
|
227
|
+
? () => {
|
|
228
|
+
if (activeSink) {
|
|
229
|
+
if (!node.sinks) node.stop = watched()
|
|
230
|
+
link(node, activeSink)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
: () => {
|
|
234
|
+
if (activeSink) link(node, activeSink)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Initialize ---
|
|
238
|
+
for (const key of Object.keys(value)) addSignal(key, value[key])
|
|
239
|
+
|
|
240
|
+
// --- Store object ---
|
|
241
|
+
const store: BaseStore<T> = {
|
|
242
|
+
[Symbol.toStringTag]: TYPE_STORE,
|
|
243
|
+
[Symbol.isConcatSpreadable]: false as const,
|
|
244
|
+
|
|
245
|
+
*[Symbol.iterator]() {
|
|
246
|
+
for (const key of Array.from(signals.keys())) {
|
|
247
|
+
const signal = signals.get(key)
|
|
248
|
+
if (signal)
|
|
249
|
+
yield [key, signal] as [
|
|
250
|
+
string,
|
|
251
|
+
(
|
|
252
|
+
| State<T[keyof T] & {}>
|
|
253
|
+
| Store<UnknownRecord>
|
|
254
|
+
| List<unknown & {}>
|
|
255
|
+
),
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
keys() {
|
|
261
|
+
subscribe()
|
|
262
|
+
return signals.keys()
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
byKey<K extends keyof T & string>(key: K) {
|
|
266
|
+
return signals.get(key) as T[K] extends readonly (infer U extends
|
|
267
|
+
{})[]
|
|
268
|
+
? List<U>
|
|
269
|
+
: T[K] extends UnknownRecord
|
|
270
|
+
? Store<T[K]>
|
|
271
|
+
: T[K] extends unknown & {}
|
|
272
|
+
? State<T[K] & {}>
|
|
273
|
+
: State<T[K] & {}> | undefined
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
get() {
|
|
277
|
+
subscribe()
|
|
278
|
+
if (node.sources) {
|
|
279
|
+
// Fast path: edges already established, rebuild value directly
|
|
280
|
+
// from child signals using untrack to avoid creating spurious
|
|
281
|
+
// edges to the current effect/memo consumer
|
|
282
|
+
if (node.flags) {
|
|
283
|
+
node.value = untrack(buildValue)
|
|
284
|
+
node.flags = FLAG_CLEAN
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// First access: use refresh() to establish child → store edges
|
|
288
|
+
refresh(node as unknown as SinkNode)
|
|
289
|
+
if (node.error) throw node.error
|
|
290
|
+
}
|
|
291
|
+
return node.value
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
set(next: T) {
|
|
295
|
+
// Use cached value if clean, recompute if dirty
|
|
296
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
297
|
+
|
|
298
|
+
const changes = diffRecords(prev, next)
|
|
299
|
+
if (applyChanges(changes)) {
|
|
300
|
+
node.flags |= FLAG_DIRTY
|
|
301
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
302
|
+
if (batchDepth === 0) flush()
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
update(fn: (prev: T) => T) {
|
|
307
|
+
store.set(fn(store.get()))
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
add<K extends keyof T & string>(key: K, value: T[K]) {
|
|
311
|
+
if (signals.has(key))
|
|
312
|
+
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
313
|
+
addSignal(key, value)
|
|
314
|
+
node.sources = null
|
|
315
|
+
node.sourcesTail = null
|
|
316
|
+
node.flags |= FLAG_DIRTY
|
|
317
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
318
|
+
if (batchDepth === 0) flush()
|
|
319
|
+
return key
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
remove(key: string) {
|
|
323
|
+
const ok = signals.delete(key)
|
|
324
|
+
if (ok) {
|
|
325
|
+
node.sources = null
|
|
326
|
+
node.sourcesTail = null
|
|
327
|
+
node.flags |= FLAG_DIRTY
|
|
328
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
329
|
+
if (batchDepth === 0) flush()
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Proxy ---
|
|
335
|
+
return new Proxy(store, {
|
|
336
|
+
get(target, prop) {
|
|
337
|
+
if (prop in target) return Reflect.get(target, prop)
|
|
338
|
+
if (typeof prop !== 'symbol')
|
|
339
|
+
return target.byKey(prop as keyof T & string)
|
|
340
|
+
},
|
|
341
|
+
has(target, prop) {
|
|
342
|
+
if (prop in target) return true
|
|
343
|
+
return target.byKey(String(prop) as keyof T & string) !== undefined
|
|
344
|
+
},
|
|
345
|
+
ownKeys(target) {
|
|
346
|
+
return Array.from(target.keys())
|
|
347
|
+
},
|
|
348
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
349
|
+
if (prop in target)
|
|
350
|
+
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
351
|
+
if (typeof prop === 'symbol') return undefined
|
|
352
|
+
const signal = target.byKey(String(prop) as keyof T & string)
|
|
353
|
+
return signal
|
|
354
|
+
? {
|
|
355
|
+
enumerable: true,
|
|
356
|
+
configurable: true,
|
|
357
|
+
writable: true,
|
|
358
|
+
value: signal,
|
|
359
|
+
}
|
|
360
|
+
: undefined
|
|
361
|
+
},
|
|
362
|
+
}) as Store<T>
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Checks if a value is a Store signal.
|
|
367
|
+
*
|
|
368
|
+
* @since 0.15.0
|
|
369
|
+
* @param value - The value to check
|
|
370
|
+
* @returns True if the value is a Store
|
|
371
|
+
*/
|
|
372
|
+
function isStore<T extends UnknownRecord>(value: unknown): value is Store<T> {
|
|
373
|
+
return isObjectOfType(value, TYPE_STORE)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* === Exports === */
|
|
377
|
+
|
|
378
|
+
export { createStore, isStore, type Store, type StoreOptions, TYPE_STORE }
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateCallback,
|
|
3
|
+
validateReadValue,
|
|
4
|
+
validateSignalValue,
|
|
5
|
+
} from '../errors'
|
|
6
|
+
import {
|
|
7
|
+
activeSink,
|
|
8
|
+
batchDepth,
|
|
9
|
+
type ComputedOptions,
|
|
10
|
+
DEFAULT_EQUALITY,
|
|
11
|
+
FLAG_DIRTY,
|
|
12
|
+
flush,
|
|
13
|
+
link,
|
|
14
|
+
propagate,
|
|
15
|
+
refresh,
|
|
16
|
+
type SinkNode,
|
|
17
|
+
type TaskCallback,
|
|
18
|
+
type TaskNode,
|
|
19
|
+
TYPE_TASK,
|
|
20
|
+
} from '../graph'
|
|
21
|
+
import { isAsyncFunction, isObjectOfType } from '../util'
|
|
22
|
+
|
|
23
|
+
/* === Types === */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An asynchronous reactive computation (colorless async).
|
|
27
|
+
* Automatically tracks dependencies and re-executes when they change.
|
|
28
|
+
* Provides abort semantics and pending state tracking.
|
|
29
|
+
*
|
|
30
|
+
* @template T - The type of value resolved by the task
|
|
31
|
+
*/
|
|
32
|
+
type Task<T extends {}> = {
|
|
33
|
+
readonly [Symbol.toStringTag]: 'Task'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the current value of the task.
|
|
37
|
+
* Returns the last resolved value, even while a new computation is pending.
|
|
38
|
+
* When called inside another reactive context, creates a dependency.
|
|
39
|
+
* @returns The current value
|
|
40
|
+
* @throws UnsetSignalValueError If the task value is still unset when read.
|
|
41
|
+
*/
|
|
42
|
+
get(): T
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Checks if the task is currently executing.
|
|
46
|
+
* @returns True if a computation is in progress
|
|
47
|
+
*/
|
|
48
|
+
isPending(): boolean
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Aborts the current computation if one is running.
|
|
52
|
+
* The task's AbortSignal will be triggered.
|
|
53
|
+
*/
|
|
54
|
+
abort(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* === Exported Functions === */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an asynchronous reactive computation (colorless async).
|
|
61
|
+
* The computation automatically tracks dependencies and re-executes when they change.
|
|
62
|
+
* Provides abort semantics - in-flight computations are aborted when dependencies change.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.18.0
|
|
65
|
+
* @template T - The type of value resolved by the task
|
|
66
|
+
* @param fn - The async computation function that receives the previous value and an AbortSignal
|
|
67
|
+
* @param options - Optional configuration for the task
|
|
68
|
+
* @param options.value - Optional initial value for reducer patterns
|
|
69
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
|
|
70
|
+
* @param options.guard - Optional type guard to validate values
|
|
71
|
+
* @param options.watched - Optional callback invoked when the task is first watched by an effect.
|
|
72
|
+
* Receives an `invalidate` function to mark the task dirty and trigger re-execution.
|
|
73
|
+
* Must return a cleanup function called when no effects are watching.
|
|
74
|
+
* @returns A Task object with get(), isPending(), and abort() methods
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const userId = createState(1);
|
|
79
|
+
* const user = createTask(async (prev, signal) => {
|
|
80
|
+
* const response = await fetch(`/api/users/${userId.get()}`, { signal });
|
|
81
|
+
* return response.json();
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* // When userId changes, the previous fetch is aborted
|
|
85
|
+
* userId.set(2);
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* // Check pending state
|
|
91
|
+
* if (user.isPending()) {
|
|
92
|
+
* console.log('Loading...');
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function createTask<T extends {}>(
|
|
97
|
+
fn: (prev: T, signal: AbortSignal) => Promise<T>,
|
|
98
|
+
options: ComputedOptions<T> & { value: T },
|
|
99
|
+
): Task<T>
|
|
100
|
+
function createTask<T extends {}>(
|
|
101
|
+
fn: TaskCallback<T>,
|
|
102
|
+
options?: ComputedOptions<T>,
|
|
103
|
+
): Task<T>
|
|
104
|
+
function createTask<T extends {}>(
|
|
105
|
+
fn: TaskCallback<T>,
|
|
106
|
+
options?: ComputedOptions<T>,
|
|
107
|
+
): Task<T> {
|
|
108
|
+
validateCallback(TYPE_TASK, fn, isAsyncFunction)
|
|
109
|
+
if (options?.value !== undefined)
|
|
110
|
+
validateSignalValue(TYPE_TASK, options.value, options?.guard)
|
|
111
|
+
|
|
112
|
+
const node: TaskNode<T> = {
|
|
113
|
+
fn,
|
|
114
|
+
value: options?.value as T,
|
|
115
|
+
sources: null,
|
|
116
|
+
sourcesTail: null,
|
|
117
|
+
sinks: null,
|
|
118
|
+
sinksTail: null,
|
|
119
|
+
flags: FLAG_DIRTY,
|
|
120
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
121
|
+
controller: undefined,
|
|
122
|
+
error: undefined,
|
|
123
|
+
stop: undefined,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const watched = options?.watched
|
|
127
|
+
const subscribe = watched
|
|
128
|
+
? () => {
|
|
129
|
+
if (activeSink) {
|
|
130
|
+
if (!node.sinks)
|
|
131
|
+
node.stop = watched(() => {
|
|
132
|
+
node.flags |= FLAG_DIRTY
|
|
133
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
134
|
+
propagate(e.sink)
|
|
135
|
+
if (batchDepth === 0) flush()
|
|
136
|
+
})
|
|
137
|
+
link(node, activeSink)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
: () => {
|
|
141
|
+
if (activeSink) link(node, activeSink)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
[Symbol.toStringTag]: TYPE_TASK,
|
|
146
|
+
get(): T {
|
|
147
|
+
subscribe()
|
|
148
|
+
refresh(node as unknown as SinkNode)
|
|
149
|
+
if (node.error) throw node.error
|
|
150
|
+
validateReadValue(TYPE_TASK, node.value)
|
|
151
|
+
return node.value
|
|
152
|
+
},
|
|
153
|
+
isPending(): boolean {
|
|
154
|
+
return !!node.controller
|
|
155
|
+
},
|
|
156
|
+
abort(): void {
|
|
157
|
+
node.controller?.abort()
|
|
158
|
+
node.controller = undefined
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks if a value is a Task signal.
|
|
165
|
+
*
|
|
166
|
+
* @since 0.18.0
|
|
167
|
+
* @param value - The value to check
|
|
168
|
+
* @returns True if the value is a Task
|
|
169
|
+
*/
|
|
170
|
+
function isTask<T extends {} = unknown & {}>(value: unknown): value is Task<T> {
|
|
171
|
+
return isObjectOfType(value, TYPE_TASK)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { createTask, isTask, type Task }
|