@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,512 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UnsetSignalValueError,
|
|
3
|
+
validateCallback,
|
|
4
|
+
validateSignalValue,
|
|
5
|
+
} from '../errors'
|
|
6
|
+
import {
|
|
7
|
+
activeSink,
|
|
8
|
+
batch,
|
|
9
|
+
type Cleanup,
|
|
10
|
+
FLAG_CLEAN,
|
|
11
|
+
FLAG_DIRTY,
|
|
12
|
+
link,
|
|
13
|
+
type MemoNode,
|
|
14
|
+
propagate,
|
|
15
|
+
refresh,
|
|
16
|
+
type Signal,
|
|
17
|
+
type SinkNode,
|
|
18
|
+
SKIP_EQUALITY,
|
|
19
|
+
TYPE_COLLECTION,
|
|
20
|
+
untrack,
|
|
21
|
+
} from '../graph'
|
|
22
|
+
import { isAsyncFunction, isObjectOfType, isSyncFunction } from '../util'
|
|
23
|
+
import {
|
|
24
|
+
getKeyGenerator,
|
|
25
|
+
isList,
|
|
26
|
+
type KeyConfig,
|
|
27
|
+
keysEqual,
|
|
28
|
+
type List,
|
|
29
|
+
} from './list'
|
|
30
|
+
import { createMemo, type Memo } from './memo'
|
|
31
|
+
import { createState, isState } from './state'
|
|
32
|
+
import { createTask } from './task'
|
|
33
|
+
|
|
34
|
+
/* === Types === */
|
|
35
|
+
|
|
36
|
+
type CollectionSource<T extends {}> = List<T> | Collection<T>
|
|
37
|
+
|
|
38
|
+
type DeriveCollectionCallback<T extends {}, U extends {}> =
|
|
39
|
+
| ((sourceValue: U) => T)
|
|
40
|
+
| ((sourceValue: U, abort: AbortSignal) => Promise<T>)
|
|
41
|
+
|
|
42
|
+
type Collection<T extends {}> = {
|
|
43
|
+
readonly [Symbol.toStringTag]: 'Collection'
|
|
44
|
+
readonly [Symbol.isConcatSpreadable]: true
|
|
45
|
+
[Symbol.iterator](): IterableIterator<Signal<T>>
|
|
46
|
+
keys(): IterableIterator<string>
|
|
47
|
+
get(): T[]
|
|
48
|
+
at(index: number): Signal<T> | undefined
|
|
49
|
+
byKey(key: string): Signal<T> | undefined
|
|
50
|
+
keyAt(index: number): string | undefined
|
|
51
|
+
indexOfKey(key: string): number
|
|
52
|
+
deriveCollection<R extends {}>(
|
|
53
|
+
callback: (sourceValue: T) => R,
|
|
54
|
+
): Collection<R>
|
|
55
|
+
deriveCollection<R extends {}>(
|
|
56
|
+
callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
|
|
57
|
+
): Collection<R>
|
|
58
|
+
readonly length: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type CollectionChanges<T> = {
|
|
62
|
+
add?: T[]
|
|
63
|
+
change?: T[]
|
|
64
|
+
remove?: T[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type CollectionOptions<T extends {}> = {
|
|
68
|
+
value?: T[]
|
|
69
|
+
keyConfig?: KeyConfig<T>
|
|
70
|
+
createItem?: (value: T) => Signal<T>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type CollectionCallback<T extends {}> = (
|
|
74
|
+
apply: (changes: CollectionChanges<T>) => void,
|
|
75
|
+
) => Cleanup
|
|
76
|
+
|
|
77
|
+
/* === Functions === */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a derived Collection from a List or another Collection with item-level memoization.
|
|
81
|
+
* Sync callbacks use createMemo, async callbacks use createTask.
|
|
82
|
+
* Structural changes are tracked reactively via the source's keys.
|
|
83
|
+
*
|
|
84
|
+
* @since 0.18.0
|
|
85
|
+
* @param source - The source List or Collection to derive from
|
|
86
|
+
* @param callback - Transformation function applied to each item
|
|
87
|
+
* @returns A Collection signal
|
|
88
|
+
*/
|
|
89
|
+
function deriveCollection<T extends {}, U extends {}>(
|
|
90
|
+
source: CollectionSource<U>,
|
|
91
|
+
callback: (sourceValue: U) => T,
|
|
92
|
+
): Collection<T>
|
|
93
|
+
function deriveCollection<T extends {}, U extends {}>(
|
|
94
|
+
source: CollectionSource<U>,
|
|
95
|
+
callback: (sourceValue: U, abort: AbortSignal) => Promise<T>,
|
|
96
|
+
): Collection<T>
|
|
97
|
+
function deriveCollection<T extends {}, U extends {}>(
|
|
98
|
+
source: CollectionSource<U>,
|
|
99
|
+
callback: DeriveCollectionCallback<T, U>,
|
|
100
|
+
): Collection<T> {
|
|
101
|
+
validateCallback(TYPE_COLLECTION, callback)
|
|
102
|
+
if (!isCollectionSource(source))
|
|
103
|
+
throw new TypeError(
|
|
104
|
+
`[${TYPE_COLLECTION}] Invalid collection source: expected a List or Collection`,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const isAsync = isAsyncFunction(callback)
|
|
108
|
+
const signals = new Map<string, Memo<T>>()
|
|
109
|
+
let keys: string[] = []
|
|
110
|
+
|
|
111
|
+
const addSignal = (key: string): void => {
|
|
112
|
+
const signal = isAsync
|
|
113
|
+
? createTask(async (prev: T | undefined, abort: AbortSignal) => {
|
|
114
|
+
const sourceValue = source.byKey(key)?.get() as U
|
|
115
|
+
if (sourceValue == null) return prev as T
|
|
116
|
+
return (
|
|
117
|
+
callback as (
|
|
118
|
+
sourceValue: U,
|
|
119
|
+
abort: AbortSignal,
|
|
120
|
+
) => Promise<T>
|
|
121
|
+
)(sourceValue, abort)
|
|
122
|
+
})
|
|
123
|
+
: createMemo(() => {
|
|
124
|
+
const sourceValue = source.byKey(key)?.get() as U
|
|
125
|
+
if (sourceValue == null) return undefined as unknown as T
|
|
126
|
+
return (callback as (sourceValue: U) => T)(sourceValue)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
signals.set(key, signal as Memo<T>)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sync signals map with source keys, reading source.keys()
|
|
133
|
+
// to establish a graph edge from source → this node.
|
|
134
|
+
// Intentionally side-effectful: mutates the private signals map and keys
|
|
135
|
+
// array. The side effects are idempotent and scoped to private state.
|
|
136
|
+
function syncKeys(): void {
|
|
137
|
+
const nextKeys = Array.from(source.keys())
|
|
138
|
+
|
|
139
|
+
if (!keysEqual(keys, nextKeys)) {
|
|
140
|
+
const a = new Set(keys)
|
|
141
|
+
const b = new Set(nextKeys)
|
|
142
|
+
|
|
143
|
+
for (const key of keys) if (!b.has(key)) signals.delete(key)
|
|
144
|
+
for (const key of nextKeys) if (!a.has(key)) addSignal(key)
|
|
145
|
+
keys = nextKeys
|
|
146
|
+
|
|
147
|
+
// Force re-establishment of edges on next refresh() so new
|
|
148
|
+
// child signals are properly linked to this node
|
|
149
|
+
node.sources = null
|
|
150
|
+
node.sourcesTail = null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build current value from child signals; syncKeys runs first to
|
|
155
|
+
// ensure the signals map is up to date and — during refresh() —
|
|
156
|
+
// to establish the graph edge from source → this node.
|
|
157
|
+
function buildValue(): T[] {
|
|
158
|
+
syncKeys()
|
|
159
|
+
const result: T[] = []
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
try {
|
|
162
|
+
const v = signals.get(key)?.get()
|
|
163
|
+
if (v != null) result.push(v)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Skip pending async items; rethrow real errors
|
|
166
|
+
if (!(e instanceof UnsetSignalValueError)) throw e
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Shallow reference equality for value arrays — prevents unnecessary
|
|
173
|
+
// downstream propagation when re-evaluation produces the same item references
|
|
174
|
+
const valuesEqual = (a: T[], b: T[]): boolean => {
|
|
175
|
+
if (a.length !== b.length) return false
|
|
176
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Structural tracking node — mirrors the List/Store/createCollection pattern.
|
|
181
|
+
// fn (buildValue) syncs keys then reads child signals to produce T[].
|
|
182
|
+
// Keys are tracked separately in a local variable.
|
|
183
|
+
const node: MemoNode<T[]> = {
|
|
184
|
+
fn: buildValue,
|
|
185
|
+
value: [],
|
|
186
|
+
flags: FLAG_DIRTY,
|
|
187
|
+
sources: null,
|
|
188
|
+
sourcesTail: null,
|
|
189
|
+
sinks: null,
|
|
190
|
+
sinksTail: null,
|
|
191
|
+
equals: valuesEqual,
|
|
192
|
+
error: undefined,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureFresh(): void {
|
|
196
|
+
if (node.sources) {
|
|
197
|
+
if (node.flags) {
|
|
198
|
+
node.value = untrack(buildValue)
|
|
199
|
+
node.flags = FLAG_CLEAN
|
|
200
|
+
// syncKeys may have nulled sources if keys changed —
|
|
201
|
+
// re-run with refresh() to establish edges to new child signals
|
|
202
|
+
if (!node.sources) {
|
|
203
|
+
node.flags = FLAG_DIRTY
|
|
204
|
+
refresh(node as unknown as SinkNode)
|
|
205
|
+
if (node.error) throw node.error
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
refresh(node as unknown as SinkNode)
|
|
210
|
+
if (node.error) throw node.error
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Initialize signals for current source keys
|
|
215
|
+
const initialKeys = Array.from(source.keys())
|
|
216
|
+
for (const key of initialKeys) addSignal(key)
|
|
217
|
+
keys = initialKeys
|
|
218
|
+
// Keep FLAG_DIRTY so the first refresh() establishes edges
|
|
219
|
+
|
|
220
|
+
const collection: Collection<T> = {
|
|
221
|
+
[Symbol.toStringTag]: TYPE_COLLECTION,
|
|
222
|
+
[Symbol.isConcatSpreadable]: true as const,
|
|
223
|
+
|
|
224
|
+
*[Symbol.iterator]() {
|
|
225
|
+
for (const key of keys) {
|
|
226
|
+
const signal = signals.get(key)
|
|
227
|
+
if (signal) yield signal
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
get length() {
|
|
232
|
+
if (activeSink) link(node, activeSink)
|
|
233
|
+
ensureFresh()
|
|
234
|
+
return keys.length
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
keys() {
|
|
238
|
+
if (activeSink) link(node, activeSink)
|
|
239
|
+
ensureFresh()
|
|
240
|
+
return keys.values()
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
get() {
|
|
244
|
+
if (activeSink) link(node, activeSink)
|
|
245
|
+
ensureFresh()
|
|
246
|
+
return node.value
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
at(index: number) {
|
|
250
|
+
return signals.get(keys[index])
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
byKey(key: string) {
|
|
254
|
+
return signals.get(key)
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
keyAt(index: number) {
|
|
258
|
+
return keys[index]
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
indexOfKey(key: string) {
|
|
262
|
+
return keys.indexOf(key)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
deriveCollection<R extends {}>(
|
|
266
|
+
cb: DeriveCollectionCallback<R, T>,
|
|
267
|
+
): Collection<R> {
|
|
268
|
+
return (
|
|
269
|
+
deriveCollection as <T2 extends {}, U2 extends {}>(
|
|
270
|
+
source: CollectionSource<U2>,
|
|
271
|
+
callback: DeriveCollectionCallback<T2, U2>,
|
|
272
|
+
) => Collection<T2>
|
|
273
|
+
)(collection, cb)
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return collection
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Creates an externally-driven Collection with a watched lifecycle.
|
|
282
|
+
* Items are managed via the `applyChanges(changes)` helper passed to the watched callback.
|
|
283
|
+
* The collection activates when first accessed by an effect and deactivates when no longer watched.
|
|
284
|
+
*
|
|
285
|
+
* @since 0.18.0
|
|
286
|
+
* @param watched - Callback invoked when the collection starts being watched, receives applyChanges helper
|
|
287
|
+
* @param options - Optional configuration including initial value, key generation, and item signal creation
|
|
288
|
+
* @returns A read-only Collection signal
|
|
289
|
+
*/
|
|
290
|
+
function createCollection<T extends {}>(
|
|
291
|
+
watched: CollectionCallback<T>,
|
|
292
|
+
options?: CollectionOptions<T>,
|
|
293
|
+
): Collection<T> {
|
|
294
|
+
const value = options?.value ?? []
|
|
295
|
+
if (value.length) validateSignalValue(TYPE_COLLECTION, value, Array.isArray)
|
|
296
|
+
validateCallback(TYPE_COLLECTION, watched, isSyncFunction)
|
|
297
|
+
|
|
298
|
+
const signals = new Map<string, Signal<T>>()
|
|
299
|
+
const keys: string[] = []
|
|
300
|
+
const itemToKey = new Map<T, string>()
|
|
301
|
+
|
|
302
|
+
const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig)
|
|
303
|
+
|
|
304
|
+
const resolveKey = (item: T): string | undefined =>
|
|
305
|
+
itemToKey.get(item) ?? (contentBased ? generateKey(item) : undefined)
|
|
306
|
+
|
|
307
|
+
const itemFactory = options?.createItem ?? createState
|
|
308
|
+
|
|
309
|
+
// Build current value from child signals
|
|
310
|
+
function buildValue(): T[] {
|
|
311
|
+
const result: T[] = []
|
|
312
|
+
for (const key of keys) {
|
|
313
|
+
try {
|
|
314
|
+
const v = signals.get(key)?.get()
|
|
315
|
+
if (v != null) result.push(v)
|
|
316
|
+
} catch (e) {
|
|
317
|
+
// Skip pending async items; rethrow real errors
|
|
318
|
+
if (!(e instanceof UnsetSignalValueError)) throw e
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const node: MemoNode<T[]> = {
|
|
325
|
+
fn: buildValue,
|
|
326
|
+
value,
|
|
327
|
+
flags: FLAG_DIRTY,
|
|
328
|
+
sources: null,
|
|
329
|
+
sourcesTail: null,
|
|
330
|
+
sinks: null,
|
|
331
|
+
sinksTail: null,
|
|
332
|
+
equals: SKIP_EQUALITY, // Always rebuild — structural changes are managed externally
|
|
333
|
+
error: undefined,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Initialize signals for initial value
|
|
337
|
+
for (const item of value) {
|
|
338
|
+
const key = generateKey(item)
|
|
339
|
+
signals.set(key, itemFactory(item))
|
|
340
|
+
itemToKey.set(item, key)
|
|
341
|
+
keys.push(key)
|
|
342
|
+
}
|
|
343
|
+
node.value = value
|
|
344
|
+
node.flags = FLAG_DIRTY // First refresh() will establish child edges
|
|
345
|
+
|
|
346
|
+
function subscribe(): void {
|
|
347
|
+
if (activeSink) {
|
|
348
|
+
if (!node.sinks)
|
|
349
|
+
node.stop = watched((changes: CollectionChanges<T>): void => {
|
|
350
|
+
const { add, change, remove } = changes
|
|
351
|
+
if (!add?.length && !change?.length && !remove?.length)
|
|
352
|
+
return
|
|
353
|
+
let structural = false
|
|
354
|
+
|
|
355
|
+
batch(() => {
|
|
356
|
+
// Additions
|
|
357
|
+
if (add) {
|
|
358
|
+
for (const item of add) {
|
|
359
|
+
const key = generateKey(item)
|
|
360
|
+
signals.set(key, itemFactory(item))
|
|
361
|
+
itemToKey.set(item, key)
|
|
362
|
+
if (!keys.includes(key)) keys.push(key)
|
|
363
|
+
structural = true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Changes — only for State signals
|
|
368
|
+
if (change) {
|
|
369
|
+
for (const item of change) {
|
|
370
|
+
const key = resolveKey(item)
|
|
371
|
+
if (!key) continue
|
|
372
|
+
const signal = signals.get(key)
|
|
373
|
+
if (signal && isState(signal)) {
|
|
374
|
+
// Update reverse map: remove old reference, add new
|
|
375
|
+
itemToKey.delete(signal.get())
|
|
376
|
+
signal.set(item)
|
|
377
|
+
itemToKey.set(item, key)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Removals
|
|
383
|
+
if (remove) {
|
|
384
|
+
for (const item of remove) {
|
|
385
|
+
const key = resolveKey(item)
|
|
386
|
+
if (!key) continue
|
|
387
|
+
itemToKey.delete(item)
|
|
388
|
+
signals.delete(key)
|
|
389
|
+
const index = keys.indexOf(key)
|
|
390
|
+
if (index !== -1) keys.splice(index, 1)
|
|
391
|
+
structural = true
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (structural) {
|
|
396
|
+
node.sources = null
|
|
397
|
+
node.sourcesTail = null
|
|
398
|
+
}
|
|
399
|
+
// Mark DIRTY so next get() rebuilds; propagate to sinks
|
|
400
|
+
node.flags = FLAG_DIRTY
|
|
401
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
402
|
+
propagate(e.sink)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
link(node, activeSink)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const collection: Collection<T> = {
|
|
410
|
+
[Symbol.toStringTag]: TYPE_COLLECTION,
|
|
411
|
+
[Symbol.isConcatSpreadable]: true as const,
|
|
412
|
+
|
|
413
|
+
*[Symbol.iterator]() {
|
|
414
|
+
for (const key of keys) {
|
|
415
|
+
const signal = signals.get(key)
|
|
416
|
+
if (signal) yield signal
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
get length() {
|
|
421
|
+
subscribe()
|
|
422
|
+
return keys.length
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
keys() {
|
|
426
|
+
subscribe()
|
|
427
|
+
return keys.values()
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
get() {
|
|
431
|
+
subscribe()
|
|
432
|
+
if (node.sources) {
|
|
433
|
+
if (node.flags) {
|
|
434
|
+
node.value = untrack(buildValue)
|
|
435
|
+
node.flags = FLAG_CLEAN
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
refresh(node as unknown as SinkNode)
|
|
439
|
+
if (node.error) throw node.error
|
|
440
|
+
}
|
|
441
|
+
return node.value
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
at(index: number) {
|
|
445
|
+
return signals.get(keys[index])
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
byKey(key: string) {
|
|
449
|
+
return signals.get(key)
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
keyAt(index: number) {
|
|
453
|
+
return keys[index]
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
indexOfKey(key: string) {
|
|
457
|
+
return keys.indexOf(key)
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
deriveCollection<R extends {}>(
|
|
461
|
+
cb: DeriveCollectionCallback<R, T>,
|
|
462
|
+
): Collection<R> {
|
|
463
|
+
return (
|
|
464
|
+
deriveCollection as <T2 extends {}, U2 extends {}>(
|
|
465
|
+
source: CollectionSource<U2>,
|
|
466
|
+
callback: DeriveCollectionCallback<T2, U2>,
|
|
467
|
+
) => Collection<T2>
|
|
468
|
+
)(collection, cb)
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return collection
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Checks if a value is a Collection signal.
|
|
477
|
+
*
|
|
478
|
+
* @since 0.17.2
|
|
479
|
+
* @param value - The value to check
|
|
480
|
+
* @returns True if the value is a Collection
|
|
481
|
+
*/
|
|
482
|
+
function isCollection<T extends {}>(value: unknown): value is Collection<T> {
|
|
483
|
+
return isObjectOfType(value, TYPE_COLLECTION)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Checks if a value is a valid Collection source (List or Collection).
|
|
488
|
+
*
|
|
489
|
+
* @since 0.17.2
|
|
490
|
+
* @param value - The value to check
|
|
491
|
+
* @returns True if the value is a List or Collection
|
|
492
|
+
*/
|
|
493
|
+
function isCollectionSource<T extends {}>(
|
|
494
|
+
value: unknown,
|
|
495
|
+
): value is CollectionSource<T> {
|
|
496
|
+
return isList(value) || isCollection(value)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* === Exports === */
|
|
500
|
+
|
|
501
|
+
export {
|
|
502
|
+
createCollection,
|
|
503
|
+
deriveCollection,
|
|
504
|
+
isCollection,
|
|
505
|
+
isCollectionSource,
|
|
506
|
+
type Collection,
|
|
507
|
+
type CollectionCallback,
|
|
508
|
+
type CollectionChanges,
|
|
509
|
+
type CollectionOptions,
|
|
510
|
+
type CollectionSource,
|
|
511
|
+
type DeriveCollectionCallback,
|
|
512
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RequiredOwnerError,
|
|
3
|
+
UnsetSignalValueError,
|
|
4
|
+
validateCallback,
|
|
5
|
+
} from '../errors'
|
|
6
|
+
import {
|
|
7
|
+
activeOwner,
|
|
8
|
+
type Cleanup,
|
|
9
|
+
type EffectCallback,
|
|
10
|
+
type EffectNode,
|
|
11
|
+
FLAG_CLEAN,
|
|
12
|
+
FLAG_DIRTY,
|
|
13
|
+
type MaybeCleanup,
|
|
14
|
+
registerCleanup,
|
|
15
|
+
runCleanup,
|
|
16
|
+
runEffect,
|
|
17
|
+
type Signal,
|
|
18
|
+
trimSources,
|
|
19
|
+
} from '../graph'
|
|
20
|
+
|
|
21
|
+
/* === Types === */
|
|
22
|
+
|
|
23
|
+
type MaybePromise<T> = T | Promise<T>
|
|
24
|
+
|
|
25
|
+
type MatchHandlers<T extends Signal<unknown & {}>[]> = {
|
|
26
|
+
ok: (values: {
|
|
27
|
+
[K in keyof T]: T[K] extends Signal<infer V> ? V : never
|
|
28
|
+
}) => MaybePromise<MaybeCleanup>
|
|
29
|
+
err?: (errors: readonly Error[]) => MaybePromise<MaybeCleanup>
|
|
30
|
+
nil?: () => MaybePromise<MaybeCleanup>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* === Exported Functions === */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a reactive effect that automatically runs when its dependencies change.
|
|
37
|
+
* Effects run immediately upon creation and re-run when any tracked signal changes.
|
|
38
|
+
* Effects are executed during the flush phase, after all updates have been batched.
|
|
39
|
+
*
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
* @param fn - The effect function that can track dependencies and register cleanup callbacks
|
|
42
|
+
* @returns A cleanup function that can be called to dispose of the effect
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const count = createState(0);
|
|
47
|
+
* const dispose = createEffect(() => {
|
|
48
|
+
* console.log('Count is:', count.get());
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* count.set(1); // Logs: "Count is: 1"
|
|
52
|
+
* dispose(); // Stop the effect
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // With cleanup
|
|
58
|
+
* createEffect(() => {
|
|
59
|
+
* const timer = setInterval(() => console.log(count.get()), 1000);
|
|
60
|
+
* return () => clearInterval(timer);
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function createEffect(fn: EffectCallback): Cleanup {
|
|
65
|
+
validateCallback('Effect', fn)
|
|
66
|
+
|
|
67
|
+
const node: EffectNode = {
|
|
68
|
+
fn,
|
|
69
|
+
flags: FLAG_DIRTY,
|
|
70
|
+
sources: null,
|
|
71
|
+
sourcesTail: null,
|
|
72
|
+
cleanup: null,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dispose = () => {
|
|
76
|
+
runCleanup(node)
|
|
77
|
+
node.fn = undefined as unknown as EffectCallback
|
|
78
|
+
node.flags = FLAG_CLEAN
|
|
79
|
+
node.sourcesTail = null
|
|
80
|
+
trimSources(node)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (activeOwner) registerCleanup(activeOwner, dispose)
|
|
84
|
+
|
|
85
|
+
runEffect(node)
|
|
86
|
+
|
|
87
|
+
return dispose
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Runs handlers based on the current values of signals.
|
|
92
|
+
* Must be called within an active owner (effect or scope) so async cleanup can be registered.
|
|
93
|
+
*
|
|
94
|
+
* @since 0.15.0
|
|
95
|
+
* @throws RequiredOwnerError If called without an active owner.
|
|
96
|
+
*/
|
|
97
|
+
function match<T extends Signal<unknown & {}>[]>(
|
|
98
|
+
signals: T,
|
|
99
|
+
handlers: MatchHandlers<T>,
|
|
100
|
+
): MaybeCleanup {
|
|
101
|
+
if (!activeOwner) throw new RequiredOwnerError('match')
|
|
102
|
+
const { ok, err = console.error, nil } = handlers
|
|
103
|
+
let errors: Error[] | undefined
|
|
104
|
+
let pending = false
|
|
105
|
+
const values = new Array(signals.length)
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < signals.length; i++) {
|
|
108
|
+
try {
|
|
109
|
+
values[i] = signals[i].get()
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (e instanceof UnsetSignalValueError) {
|
|
112
|
+
pending = true
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (!errors) errors = []
|
|
116
|
+
errors.push(e instanceof Error ? e : new Error(String(e)))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let out: MaybePromise<MaybeCleanup>
|
|
121
|
+
try {
|
|
122
|
+
if (pending) out = nil?.()
|
|
123
|
+
else if (errors) out = err(errors)
|
|
124
|
+
else
|
|
125
|
+
out = ok(
|
|
126
|
+
values as {
|
|
127
|
+
[K in keyof T]: T[K] extends Signal<infer V> ? V : never
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
err([e instanceof Error ? e : new Error(String(e))])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof out === 'function') return out
|
|
135
|
+
|
|
136
|
+
if (out instanceof Promise) {
|
|
137
|
+
const owner = activeOwner
|
|
138
|
+
const controller = new AbortController()
|
|
139
|
+
registerCleanup(owner, () => controller.abort())
|
|
140
|
+
out.then(cleanup => {
|
|
141
|
+
if (!controller.signal.aborted && typeof cleanup === 'function')
|
|
142
|
+
registerCleanup(owner, cleanup)
|
|
143
|
+
}).catch(e => {
|
|
144
|
+
err([e instanceof Error ? e : new Error(String(e))])
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { type MaybePromise, type MatchHandlers, createEffect, match }
|