@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.
Files changed (94) hide show
  1. package/.ai-context.md +169 -227
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +176 -116
  4. package/ARCHITECTURE.md +276 -0
  5. package/CHANGELOG.md +29 -0
  6. package/CLAUDE.md +201 -143
  7. package/GUIDE.md +298 -0
  8. package/README.md +246 -193
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/context7.json +4 -0
  12. package/examples/events-sensor.ts +187 -0
  13. package/examples/selector-sensor.ts +173 -0
  14. package/index.dev.js +1390 -1008
  15. package/index.js +1 -1
  16. package/index.ts +60 -74
  17. package/package.json +5 -2
  18. package/skills/changelog-keeper/SKILL.md +59 -0
  19. package/skills/changelog-keeper/agents/openai.yaml +4 -0
  20. package/src/errors.ts +118 -74
  21. package/src/graph.ts +612 -0
  22. package/src/nodes/collection.ts +512 -0
  23. package/src/nodes/effect.ts +149 -0
  24. package/src/nodes/list.ts +589 -0
  25. package/src/nodes/memo.ts +148 -0
  26. package/src/nodes/sensor.ts +149 -0
  27. package/src/nodes/state.ts +135 -0
  28. package/src/nodes/store.ts +378 -0
  29. package/src/nodes/task.ts +174 -0
  30. package/src/signal.ts +112 -66
  31. package/src/util.ts +26 -57
  32. package/test/batch.test.ts +96 -62
  33. package/test/benchmark.test.ts +473 -487
  34. package/test/collection.test.ts +456 -707
  35. package/test/effect.test.ts +293 -696
  36. package/test/list.test.ts +335 -592
  37. package/test/memo.test.ts +574 -0
  38. package/test/regression.test.ts +156 -0
  39. package/test/scope.test.ts +191 -0
  40. package/test/sensor.test.ts +454 -0
  41. package/test/signal.test.ts +220 -213
  42. package/test/state.test.ts +217 -265
  43. package/test/store.test.ts +346 -446
  44. package/test/task.test.ts +529 -0
  45. package/test/untrack.test.ts +167 -0
  46. package/types/index.d.ts +13 -15
  47. package/types/src/errors.d.ts +73 -17
  48. package/types/src/graph.d.ts +218 -0
  49. package/types/src/nodes/collection.d.ts +69 -0
  50. package/types/src/nodes/effect.d.ts +48 -0
  51. package/types/src/nodes/list.d.ts +66 -0
  52. package/types/src/nodes/memo.d.ts +63 -0
  53. package/types/src/nodes/sensor.d.ts +81 -0
  54. package/types/src/nodes/state.d.ts +78 -0
  55. package/types/src/nodes/store.d.ts +51 -0
  56. package/types/src/nodes/task.d.ts +79 -0
  57. package/types/src/signal.d.ts +43 -29
  58. package/types/src/util.d.ts +9 -16
  59. package/archive/benchmark.ts +0 -683
  60. package/archive/collection.ts +0 -253
  61. package/archive/composite.ts +0 -85
  62. package/archive/computed.ts +0 -195
  63. package/archive/list.ts +0 -483
  64. package/archive/memo.ts +0 -139
  65. package/archive/state.ts +0 -90
  66. package/archive/store.ts +0 -298
  67. package/archive/task.ts +0 -189
  68. package/src/classes/collection.ts +0 -245
  69. package/src/classes/computed.ts +0 -349
  70. package/src/classes/list.ts +0 -343
  71. package/src/classes/ref.ts +0 -70
  72. package/src/classes/state.ts +0 -102
  73. package/src/classes/store.ts +0 -262
  74. package/src/diff.ts +0 -138
  75. package/src/effect.ts +0 -93
  76. package/src/match.ts +0 -45
  77. package/src/resolve.ts +0 -49
  78. package/src/system.ts +0 -257
  79. package/test/computed.test.ts +0 -1108
  80. package/test/diff.test.ts +0 -955
  81. package/test/match.test.ts +0 -388
  82. package/test/ref.test.ts +0 -353
  83. package/test/resolve.test.ts +0 -154
  84. package/types/src/classes/collection.d.ts +0 -45
  85. package/types/src/classes/computed.d.ts +0 -94
  86. package/types/src/classes/list.d.ts +0 -43
  87. package/types/src/classes/ref.d.ts +0 -35
  88. package/types/src/classes/state.d.ts +0 -49
  89. package/types/src/classes/store.d.ts +0 -52
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. 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 }