@zeix/cause-effect 0.17.3 → 0.18.0

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 (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,474 @@
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
+ TYPE_COLLECTION,
19
+ untrack,
20
+ } from '../graph'
21
+ import { isAsyncFunction, isFunction, isObjectOfType } from '../util'
22
+ import {
23
+ type DiffResult,
24
+ isList,
25
+ type KeyConfig,
26
+ keysEqual,
27
+ type List,
28
+ } from './list'
29
+ import { createMemo, type Memo } from './memo'
30
+ import { createState, isState } from './state'
31
+ import { createTask } from './task'
32
+
33
+ /* === Types === */
34
+
35
+ type CollectionSource<T extends {}> = List<T> | Collection<T>
36
+
37
+ type DeriveCollectionCallback<T extends {}, U extends {}> =
38
+ | ((sourceValue: U) => T)
39
+ | ((sourceValue: U, abort: AbortSignal) => Promise<T>)
40
+
41
+ type Collection<T extends {}> = {
42
+ readonly [Symbol.toStringTag]: 'Collection'
43
+ readonly [Symbol.isConcatSpreadable]: true
44
+ [Symbol.iterator](): IterableIterator<Signal<T>>
45
+ keys(): IterableIterator<string>
46
+ get(): T[]
47
+ at(index: number): Signal<T> | undefined
48
+ byKey(key: string): Signal<T> | undefined
49
+ keyAt(index: number): string | undefined
50
+ indexOfKey(key: string): number
51
+ deriveCollection<R extends {}>(
52
+ callback: (sourceValue: T) => R,
53
+ ): Collection<R>
54
+ deriveCollection<R extends {}>(
55
+ callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
56
+ ): Collection<R>
57
+ readonly length: number
58
+ }
59
+
60
+ type CollectionOptions<T extends {}> = {
61
+ value?: T[]
62
+ keyConfig?: KeyConfig<T>
63
+ createItem?: (key: string, value: T) => Signal<T>
64
+ }
65
+
66
+ type CollectionCallback = (
67
+ applyChanges: (changes: DiffResult) => void,
68
+ ) => Cleanup
69
+
70
+ /* === Functions === */
71
+
72
+ /**
73
+ * Creates a derived Collection from a List or another Collection with item-level memoization.
74
+ * Sync callbacks use createMemo, async callbacks use createTask.
75
+ * Structural changes are tracked reactively via the source's keys.
76
+ *
77
+ * @since 0.18.0
78
+ * @param source - The source List or Collection to derive from
79
+ * @param callback - Transformation function applied to each item
80
+ * @returns A Collection signal
81
+ */
82
+ function deriveCollection<T extends {}, U extends {}>(
83
+ source: CollectionSource<U>,
84
+ callback: (sourceValue: U) => T,
85
+ ): Collection<T>
86
+ function deriveCollection<T extends {}, U extends {}>(
87
+ source: CollectionSource<U>,
88
+ callback: (sourceValue: U, abort: AbortSignal) => Promise<T>,
89
+ ): Collection<T>
90
+ function deriveCollection<T extends {}, U extends {}>(
91
+ source: CollectionSource<U>,
92
+ callback: DeriveCollectionCallback<T, U>,
93
+ ): Collection<T> {
94
+ validateCallback(TYPE_COLLECTION, callback)
95
+ if (!isCollectionSource(source))
96
+ throw new TypeError(
97
+ `[${TYPE_COLLECTION}] Invalid collection source: expected a List or Collection`,
98
+ )
99
+
100
+ const isAsync = isAsyncFunction(callback)
101
+ const signals = new Map<string, Memo<T>>()
102
+
103
+ const addSignal = (key: string): void => {
104
+ const signal = isAsync
105
+ ? createTask(async (prev: T | undefined, abort: AbortSignal) => {
106
+ const sourceValue = source.byKey(key)?.get() as U
107
+ if (sourceValue == null) return prev as T
108
+ return (
109
+ callback as (
110
+ sourceValue: U,
111
+ abort: AbortSignal,
112
+ ) => Promise<T>
113
+ )(sourceValue, abort)
114
+ })
115
+ : createMemo(() => {
116
+ const sourceValue = source.byKey(key)?.get() as U
117
+ if (sourceValue == null) return undefined as unknown as T
118
+ return (callback as (sourceValue: U) => T)(sourceValue)
119
+ })
120
+
121
+ signals.set(key, signal as Memo<T>)
122
+ }
123
+
124
+ // Sync collection signals with source keys, reading source.keys()
125
+ // to establish a graph edge from source → this node.
126
+ // Intentionally side-effectful: mutates the private signals map inside what
127
+ // is conceptually a MemoNode.fn. The side effects are idempotent and scoped
128
+ // to private state — separating detection from application would add complexity.
129
+ function syncKeys(): string[] {
130
+ const newKeys = Array.from(source.keys())
131
+ const oldKeys = node.value
132
+
133
+ if (!keysEqual(oldKeys, newKeys)) {
134
+ const oldKeySet = new Set(oldKeys)
135
+ const newKeySet = new Set(newKeys)
136
+
137
+ for (const key of oldKeys)
138
+ if (!newKeySet.has(key)) signals.delete(key)
139
+ for (const key of newKeys) if (!oldKeySet.has(key)) addSignal(key)
140
+ }
141
+
142
+ return newKeys
143
+ }
144
+
145
+ // Structural tracking node — not a general-purpose Memo.
146
+ // fn (syncKeys) reads source.keys() to detect additions/removals.
147
+ // Value is a string[] of keys, not the collection's actual values.
148
+ const node: MemoNode<string[]> = {
149
+ fn: syncKeys,
150
+ value: [],
151
+ flags: FLAG_DIRTY,
152
+ sources: null,
153
+ sourcesTail: null,
154
+ sinks: null,
155
+ sinksTail: null,
156
+ equals: keysEqual,
157
+ error: undefined,
158
+ }
159
+
160
+ // Ensure keys are synced, using the same pattern as List/Store
161
+ function ensureSynced(): string[] {
162
+ if (node.sources) {
163
+ if (node.flags) {
164
+ node.value = untrack(syncKeys)
165
+ node.flags = FLAG_CLEAN
166
+ }
167
+ } else {
168
+ refresh(node as unknown as SinkNode)
169
+ if (node.error) throw node.error
170
+ }
171
+ return node.value
172
+ }
173
+
174
+ // Initialize signals for current source keys
175
+ const initialKeys = Array.from(source.keys())
176
+ for (const key of initialKeys) addSignal(key)
177
+ node.value = initialKeys
178
+ // Keep FLAG_DIRTY so the first refresh() establishes the edge to the source
179
+
180
+ const collection: Collection<T> = {
181
+ [Symbol.toStringTag]: TYPE_COLLECTION,
182
+ [Symbol.isConcatSpreadable]: true as const,
183
+
184
+ *[Symbol.iterator]() {
185
+ for (const key of node.value) {
186
+ const signal = signals.get(key)
187
+ if (signal) yield signal
188
+ }
189
+ },
190
+
191
+ get length() {
192
+ if (activeSink) link(node, activeSink)
193
+ return ensureSynced().length
194
+ },
195
+
196
+ keys() {
197
+ if (activeSink) link(node, activeSink)
198
+ return ensureSynced().values()
199
+ },
200
+
201
+ get() {
202
+ if (activeSink) link(node, activeSink)
203
+ const currentKeys = ensureSynced()
204
+ const result: T[] = []
205
+ for (const key of currentKeys) {
206
+ try {
207
+ const v = signals.get(key)?.get()
208
+ if (v != null) result.push(v)
209
+ } catch (e) {
210
+ // Skip pending async items; rethrow real errors
211
+ if (!(e instanceof UnsetSignalValueError)) throw e
212
+ }
213
+ }
214
+ return result
215
+ },
216
+
217
+ at(index: number) {
218
+ return signals.get(node.value[index])
219
+ },
220
+
221
+ byKey(key: string) {
222
+ return signals.get(key)
223
+ },
224
+
225
+ keyAt(index: number) {
226
+ return node.value[index]
227
+ },
228
+
229
+ indexOfKey(key: string) {
230
+ return node.value.indexOf(key)
231
+ },
232
+
233
+ deriveCollection<R extends {}>(
234
+ cb: DeriveCollectionCallback<R, T>,
235
+ ): Collection<R> {
236
+ return (
237
+ deriveCollection as <T2 extends {}, U2 extends {}>(
238
+ source: CollectionSource<U2>,
239
+ callback: DeriveCollectionCallback<T2, U2>,
240
+ ) => Collection<T2>
241
+ )(collection, cb)
242
+ },
243
+ }
244
+
245
+ return collection
246
+ }
247
+
248
+ /**
249
+ * Creates an externally-driven Collection with a watched lifecycle.
250
+ * Items are managed by the start callback via `applyChanges(diffResult)`.
251
+ * The collection activates when first accessed by an effect and deactivates when no longer watched.
252
+ *
253
+ * @since 0.18.0
254
+ * @param start - Callback invoked when the collection starts being watched, receives applyChanges helper
255
+ * @param options - Optional configuration including initial value, key generation, and item signal creation
256
+ * @returns A read-only Collection signal
257
+ */
258
+ function createCollection<T extends {}>(
259
+ start: CollectionCallback,
260
+ options?: CollectionOptions<T>,
261
+ ): Collection<T> {
262
+ const initialValue = options?.value ?? []
263
+ if (initialValue.length)
264
+ validateSignalValue(TYPE_COLLECTION, initialValue, Array.isArray)
265
+ validateCallback(TYPE_COLLECTION, start)
266
+
267
+ const signals = new Map<string, Signal<T>>()
268
+ const keys: string[] = []
269
+
270
+ let keyCounter = 0
271
+ const keyConfig = options?.keyConfig
272
+ const generateKey: (item: T) => string =
273
+ typeof keyConfig === 'string'
274
+ ? () => `${keyConfig}${keyCounter++}`
275
+ : isFunction<string>(keyConfig)
276
+ ? (item: T) => keyConfig(item)
277
+ : () => String(keyCounter++)
278
+
279
+ const itemFactory =
280
+ options?.createItem ?? ((_key: string, value: T) => createState(value))
281
+
282
+ // Build current value from child signals
283
+ function buildValue(): T[] {
284
+ const result: T[] = []
285
+ for (const key of keys) {
286
+ try {
287
+ const v = signals.get(key)?.get()
288
+ if (v != null) result.push(v)
289
+ } catch (e) {
290
+ // Skip pending async items; rethrow real errors
291
+ if (!(e instanceof UnsetSignalValueError)) throw e
292
+ }
293
+ }
294
+ return result
295
+ }
296
+
297
+ const node: MemoNode<T[]> = {
298
+ fn: buildValue,
299
+ value: initialValue,
300
+ flags: FLAG_DIRTY,
301
+ sources: null,
302
+ sourcesTail: null,
303
+ sinks: null,
304
+ sinksTail: null,
305
+ equals: () => false, // Always rebuild — structural changes are managed externally
306
+ error: undefined,
307
+ }
308
+
309
+ /** Apply external changes to the collection */
310
+ function applyChanges(changes: DiffResult): void {
311
+ if (!changes.changed) return
312
+ let structural = false
313
+
314
+ batch(() => {
315
+ // Additions
316
+ for (const key in changes.add) {
317
+ const value = changes.add[key] as T
318
+ signals.set(key, itemFactory(key, value))
319
+ if (!keys.includes(key)) keys.push(key)
320
+ structural = true
321
+ }
322
+
323
+ // Changes — only for State signals
324
+ for (const key in changes.change) {
325
+ const signal = signals.get(key)
326
+ if (signal && isState(signal)) {
327
+ signal.set(changes.change[key] as T)
328
+ }
329
+ }
330
+
331
+ // Removals
332
+ for (const key in changes.remove) {
333
+ signals.delete(key)
334
+ const index = keys.indexOf(key)
335
+ if (index !== -1) keys.splice(index, 1)
336
+ structural = true
337
+ }
338
+
339
+ if (structural) {
340
+ node.sources = null
341
+ node.sourcesTail = null
342
+ }
343
+ // Reset CHECK/RUNNING before propagate; mark DIRTY so next get() rebuilds
344
+ node.flags = FLAG_CLEAN
345
+ propagate(node as unknown as SinkNode)
346
+ node.flags |= FLAG_DIRTY
347
+ })
348
+ }
349
+
350
+ // Initialize signals for initial value
351
+ for (const item of initialValue) {
352
+ const key = generateKey(item)
353
+ signals.set(key, itemFactory(key, item))
354
+ keys.push(key)
355
+ }
356
+ node.value = initialValue
357
+ node.flags = FLAG_DIRTY // First refresh() will establish child edges
358
+
359
+ function startWatching(): void {
360
+ if (!node.sinks) node.stop = start(applyChanges)
361
+ }
362
+
363
+ const collection: Collection<T> = {
364
+ [Symbol.toStringTag]: TYPE_COLLECTION,
365
+ [Symbol.isConcatSpreadable]: true as const,
366
+
367
+ *[Symbol.iterator]() {
368
+ for (const key of keys) {
369
+ const signal = signals.get(key)
370
+ if (signal) yield signal
371
+ }
372
+ },
373
+
374
+ get length() {
375
+ if (activeSink) {
376
+ startWatching()
377
+ link(node, activeSink)
378
+ }
379
+ return keys.length
380
+ },
381
+
382
+ keys() {
383
+ if (activeSink) {
384
+ startWatching()
385
+ link(node, activeSink)
386
+ }
387
+ return keys.values()
388
+ },
389
+
390
+ get() {
391
+ if (activeSink) {
392
+ startWatching()
393
+ link(node, activeSink)
394
+ }
395
+ if (node.sources) {
396
+ if (node.flags) {
397
+ node.value = untrack(buildValue)
398
+ node.flags = FLAG_CLEAN
399
+ }
400
+ } else {
401
+ refresh(node as unknown as SinkNode)
402
+ if (node.error) throw node.error
403
+ }
404
+ return node.value
405
+ },
406
+
407
+ at(index: number) {
408
+ return signals.get(keys[index])
409
+ },
410
+
411
+ byKey(key: string) {
412
+ return signals.get(key)
413
+ },
414
+
415
+ keyAt(index: number) {
416
+ return keys[index]
417
+ },
418
+
419
+ indexOfKey(key: string) {
420
+ return keys.indexOf(key)
421
+ },
422
+
423
+ deriveCollection<R extends {}>(
424
+ cb: DeriveCollectionCallback<R, T>,
425
+ ): Collection<R> {
426
+ return (
427
+ deriveCollection as <T2 extends {}, U2 extends {}>(
428
+ source: CollectionSource<U2>,
429
+ callback: DeriveCollectionCallback<T2, U2>,
430
+ ) => Collection<T2>
431
+ )(collection, cb)
432
+ },
433
+ }
434
+
435
+ return collection
436
+ }
437
+
438
+ /**
439
+ * Checks if a value is a Collection signal.
440
+ *
441
+ * @since 0.17.2
442
+ * @param value - The value to check
443
+ * @returns True if the value is a Collection
444
+ */
445
+ function isCollection<T extends {}>(value: unknown): value is Collection<T> {
446
+ return isObjectOfType(value, TYPE_COLLECTION)
447
+ }
448
+
449
+ /**
450
+ * Checks if a value is a valid Collection source (List or Collection).
451
+ *
452
+ * @since 0.17.2
453
+ * @param value - The value to check
454
+ * @returns True if the value is a List or Collection
455
+ */
456
+ function isCollectionSource<T extends {}>(
457
+ value: unknown,
458
+ ): value is CollectionSource<T> {
459
+ return isList(value) || isCollection(value)
460
+ }
461
+
462
+ /* === Exports === */
463
+
464
+ export {
465
+ createCollection,
466
+ deriveCollection,
467
+ isCollection,
468
+ isCollectionSource,
469
+ type Collection,
470
+ type CollectionCallback,
471
+ type CollectionOptions,
472
+ type CollectionSource,
473
+ type DeriveCollectionCallback,
474
+ }
@@ -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 }