@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,588 @@
1
+ import {
2
+ CircularDependencyError,
3
+ DuplicateKeyError,
4
+ validateSignalValue,
5
+ } from '../errors'
6
+ import {
7
+ activeSink,
8
+ batch,
9
+ batchDepth,
10
+ type Cleanup,
11
+ FLAG_CLEAN,
12
+ FLAG_DIRTY,
13
+ flush,
14
+ link,
15
+ type MemoNode,
16
+ propagate,
17
+ refresh,
18
+ type SinkNode,
19
+ TYPE_LIST,
20
+ untrack,
21
+ } from '../graph'
22
+ import { isFunction, isObjectOfType, isRecord } from '../util'
23
+ import {
24
+ type Collection,
25
+ type CollectionSource,
26
+ type DeriveCollectionCallback,
27
+ deriveCollection,
28
+ } from './collection'
29
+ import { createState, type State } from './state'
30
+
31
+ /* === Types === */
32
+
33
+ type UnknownRecord = Record<string, unknown>
34
+
35
+ type DiffResult = {
36
+ changed: boolean
37
+ add: UnknownRecord
38
+ change: UnknownRecord
39
+ remove: UnknownRecord
40
+ }
41
+
42
+ type KeyConfig<T> = string | ((item: T) => string)
43
+
44
+ type ListOptions<T extends {}> = {
45
+ keyConfig?: KeyConfig<T>
46
+ watched?: () => Cleanup
47
+ }
48
+
49
+ type List<T extends {}> = {
50
+ readonly [Symbol.toStringTag]: 'List'
51
+ readonly [Symbol.isConcatSpreadable]: true
52
+ [Symbol.iterator](): IterableIterator<State<T>>
53
+ readonly length: number
54
+ get(): T[]
55
+ set(newValue: T[]): void
56
+ update(fn: (oldValue: T[]) => T[]): void
57
+ at(index: number): State<T> | undefined
58
+ keys(): IterableIterator<string>
59
+ byKey(key: string): State<T> | undefined
60
+ keyAt(index: number): string | undefined
61
+ indexOfKey(key: string): number
62
+ add(value: T): string
63
+ remove(keyOrIndex: string | number): void
64
+ sort(compareFn?: (a: T, b: T) => number): void
65
+ splice(start: number, deleteCount?: number, ...items: T[]): T[]
66
+ deriveCollection<R extends {}>(
67
+ callback: (sourceValue: T) => R,
68
+ ): Collection<R>
69
+ deriveCollection<R extends {}>(
70
+ callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
71
+ ): Collection<R>
72
+ }
73
+
74
+ /* === Functions === */
75
+
76
+ /**
77
+ * Checks if two values are equal with cycle detection
78
+ *
79
+ * @since 0.15.0
80
+ * @param {T} a - First value to compare
81
+ * @param {T} b - Second value to compare
82
+ * @param {WeakSet<object>} visited - Set to track visited objects for cycle detection
83
+ * @returns {boolean} Whether the two values are equal
84
+ */
85
+
86
+ /** Shallow equality check for string arrays */
87
+ function keysEqual(a: string[], b: string[]): boolean {
88
+ if (a.length !== b.length) return false
89
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
90
+ return true
91
+ }
92
+
93
+ function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
94
+ // Fast paths
95
+ if (Object.is(a, b)) return true
96
+ if (typeof a !== typeof b) return false
97
+ if (
98
+ a == null ||
99
+ typeof a !== 'object' ||
100
+ b == null ||
101
+ typeof b !== 'object'
102
+ )
103
+ return false
104
+
105
+ // Cycle detection (only allocate WeakSet when both values are objects)
106
+ if (!visited) visited = new WeakSet()
107
+ if (visited.has(a as object) || visited.has(b as object))
108
+ throw new CircularDependencyError('isEqual')
109
+ visited.add(a)
110
+ visited.add(b)
111
+
112
+ try {
113
+ const aIsArray = Array.isArray(a)
114
+ if (aIsArray !== Array.isArray(b)) return false
115
+
116
+ if (aIsArray) {
117
+ const aa = a as unknown[]
118
+ const ba = b as unknown[]
119
+ if (aa.length !== ba.length) return false
120
+ for (let i = 0; i < aa.length; i++) {
121
+ if (!isEqual(aa[i], ba[i], visited)) return false
122
+ }
123
+ return true
124
+ }
125
+
126
+ if (isRecord(a) && isRecord(b)) {
127
+ const aKeys = Object.keys(a)
128
+ const bKeys = Object.keys(b)
129
+
130
+ if (aKeys.length !== bKeys.length) return false
131
+ for (const key of aKeys) {
132
+ if (!(key in b)) return false
133
+ if (!isEqual(a[key], b[key], visited)) return false
134
+ }
135
+ return true
136
+ }
137
+
138
+ // For non-records/non-arrays, they are only equal if they are the same reference
139
+ // (which would have been caught by Object.is at the beginning)
140
+ return false
141
+ } finally {
142
+ visited.delete(a)
143
+ visited.delete(b)
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Compares two arrays using existing keys and returns differences as a DiffResult.
149
+ * Avoids object conversion by working directly with arrays and keys.
150
+ *
151
+ * @since 0.18.0
152
+ * @param {T[]} oldArray - The old array
153
+ * @param {T[]} newArray - The new array
154
+ * @param {string[]} currentKeys - Current keys array (may be sparse or shorter than oldArray)
155
+ * @param {(item: T) => string} generateKey - Function to generate keys for new items
156
+ * @param {boolean} contentBased - When true, always use generateKey (content-based keys);
157
+ * when false, reuse positional keys from currentKeys (synthetic keys)
158
+ * @returns {DiffResult & { newKeys: string[] }} The differences in DiffResult format plus updated keys array
159
+ */
160
+ function diffArrays<T>(
161
+ oldArray: T[],
162
+ newArray: T[],
163
+ currentKeys: string[],
164
+ generateKey: (item: T) => string,
165
+ contentBased: boolean,
166
+ ): DiffResult & { newKeys: string[] } {
167
+ const visited = new WeakSet()
168
+ const add = {} as UnknownRecord
169
+ const change = {} as UnknownRecord
170
+ const remove = {} as UnknownRecord
171
+ const newKeys: string[] = []
172
+ let changed = false
173
+
174
+ // Build a map of old values by key for quick lookup
175
+ const oldByKey = new Map<string, T>()
176
+ for (let i = 0; i < oldArray.length; i++) {
177
+ const key = currentKeys[i]
178
+ if (key && oldArray[i]) oldByKey.set(key, oldArray[i])
179
+ }
180
+
181
+ // Track which old keys we've seen
182
+ const seenKeys = new Set<string>()
183
+
184
+ // Process new array and build new keys array
185
+ for (let i = 0; i < newArray.length; i++) {
186
+ const newValue = newArray[i]
187
+ if (newValue === undefined) continue
188
+
189
+ // Content-based keys: always derive from item; synthetic keys: reuse by position
190
+ const key = contentBased
191
+ ? generateKey(newValue)
192
+ : (currentKeys[i] ?? generateKey(newValue))
193
+
194
+ if (seenKeys.has(key))
195
+ throw new DuplicateKeyError(TYPE_LIST, key, newValue)
196
+
197
+ newKeys.push(key)
198
+ seenKeys.add(key)
199
+
200
+ // Check if this key existed before
201
+ if (!oldByKey.has(key)) {
202
+ add[key] = newValue
203
+ changed = true
204
+ } else {
205
+ const oldValue = oldByKey.get(key)
206
+ if (!isEqual(oldValue, newValue, visited)) {
207
+ change[key] = newValue
208
+ changed = true
209
+ }
210
+ }
211
+ }
212
+
213
+ // Find removed keys (existed in old but not in new)
214
+ for (const [key] of oldByKey) {
215
+ if (!seenKeys.has(key)) {
216
+ remove[key] = null
217
+ changed = true
218
+ }
219
+ }
220
+
221
+ // Detect reorder even when no values changed
222
+ if (!changed && !keysEqual(currentKeys, newKeys)) changed = true
223
+
224
+ return { add, change, remove, newKeys, changed }
225
+ }
226
+
227
+ /**
228
+ * Creates a reactive list with stable keys and per-item reactivity.
229
+ *
230
+ * @since 0.18.0
231
+ * @param initialValue - Initial array of items
232
+ * @param options - Optional configuration for key generation and watch lifecycle
233
+ * @returns A List signal
234
+ */
235
+ function createList<T extends {}>(
236
+ initialValue: T[],
237
+ options?: ListOptions<T>,
238
+ ): List<T> {
239
+ validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
240
+
241
+ const signals = new Map<string, State<T>>()
242
+ let keys: string[] = []
243
+
244
+ let keyCounter = 0
245
+ const keyConfig = options?.keyConfig
246
+ const contentBased = isFunction<string>(keyConfig)
247
+ const generateKey: (item: T) => string =
248
+ typeof keyConfig === 'string'
249
+ ? () => `${keyConfig}${keyCounter++}`
250
+ : contentBased
251
+ ? (item: T) => keyConfig(item)
252
+ : () => String(keyCounter++)
253
+
254
+ // --- Internal helpers ---
255
+
256
+ // Build current value from child signals
257
+ const buildValue = (): T[] =>
258
+ keys
259
+ .map(key => signals.get(key)?.get())
260
+ .filter(v => v !== undefined) as T[]
261
+
262
+ // Structural tracking node — not a general-purpose Memo.
263
+ // On first get(): refresh() establishes edges from child signals.
264
+ // On subsequent get(): untrack(buildValue) rebuilds without re-linking.
265
+ // Mutation methods (add/remove/set/splice) null out sources to force re-establishment.
266
+ const node: MemoNode<T[]> = {
267
+ fn: buildValue,
268
+ value: initialValue,
269
+ flags: FLAG_DIRTY,
270
+ sources: null,
271
+ sourcesTail: null,
272
+ sinks: null,
273
+ sinksTail: null,
274
+ equals: isEqual,
275
+ error: undefined,
276
+ }
277
+
278
+ const toRecord = (array: T[]): Record<string, T> => {
279
+ const record = {} as Record<string, T>
280
+ for (let i = 0; i < array.length; i++) {
281
+ const value = array[i]
282
+ if (value === undefined) continue
283
+ let key = keys[i]
284
+ if (!key) {
285
+ key = generateKey(value)
286
+ keys[i] = key
287
+ }
288
+ record[key] = value
289
+ }
290
+ return record
291
+ }
292
+
293
+ const applyChanges = (changes: DiffResult): boolean => {
294
+ let structural = false
295
+
296
+ // Additions
297
+ for (const key in changes.add) {
298
+ const value = changes.add[key] as T
299
+ validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
300
+ signals.set(key, createState(value))
301
+ structural = true
302
+ }
303
+
304
+ // Changes
305
+ if (Object.keys(changes.change).length) {
306
+ batch(() => {
307
+ for (const key in changes.change) {
308
+ const value = changes.change[key]
309
+ validateSignalValue(
310
+ `${TYPE_LIST} item for key "${key}"`,
311
+ value,
312
+ )
313
+ const signal = signals.get(key)
314
+ if (signal) signal.set(value as T)
315
+ }
316
+ })
317
+ }
318
+
319
+ // Removals
320
+ for (const key in changes.remove) {
321
+ signals.delete(key)
322
+ const index = keys.indexOf(key)
323
+ if (index !== -1) keys.splice(index, 1)
324
+ structural = true
325
+ }
326
+
327
+ if (structural) {
328
+ node.sources = null
329
+ node.sourcesTail = null
330
+ }
331
+
332
+ return changes.changed
333
+ }
334
+
335
+ // --- Initialize ---
336
+ const initRecord = toRecord(initialValue)
337
+ for (const key in initRecord) {
338
+ const value = initRecord[key]
339
+ validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
340
+ signals.set(key, createState(value))
341
+ }
342
+
343
+ // Starts clean: mutation methods (add/remove/set/splice) explicitly call
344
+ // propagate() + invalidate edges, so refresh() on first get() is not needed.
345
+ node.value = initialValue
346
+ node.flags = 0
347
+
348
+ // --- List object ---
349
+ const list: List<T> = {
350
+ [Symbol.toStringTag]: TYPE_LIST,
351
+ [Symbol.isConcatSpreadable]: true as const,
352
+
353
+ *[Symbol.iterator]() {
354
+ for (const key of keys) {
355
+ const signal = signals.get(key)
356
+ if (signal) yield signal
357
+ }
358
+ },
359
+
360
+ get length() {
361
+ if (activeSink) {
362
+ if (!node.sinks && options?.watched)
363
+ node.stop = options.watched()
364
+ link(node, activeSink)
365
+ }
366
+ return keys.length
367
+ },
368
+
369
+ get() {
370
+ if (activeSink) {
371
+ if (!node.sinks && options?.watched)
372
+ node.stop = options.watched()
373
+ link(node, activeSink)
374
+ }
375
+ if (node.sources) {
376
+ // Fast path: edges already established, rebuild value directly
377
+ if (node.flags) {
378
+ node.value = untrack(buildValue)
379
+ node.flags = FLAG_CLEAN
380
+ }
381
+ } else {
382
+ // First access: use refresh() to establish child → list edges
383
+ refresh(node as unknown as SinkNode)
384
+ if (node.error) throw node.error
385
+ }
386
+ return node.value
387
+ },
388
+
389
+ set(newValue: T[]) {
390
+ const currentValue =
391
+ node.flags & FLAG_DIRTY ? buildValue() : node.value
392
+ const changes = diffArrays(
393
+ currentValue,
394
+ newValue,
395
+ keys,
396
+ generateKey,
397
+ contentBased,
398
+ )
399
+ if (changes.changed) {
400
+ keys = changes.newKeys
401
+ applyChanges(changes)
402
+ propagate(node as unknown as SinkNode)
403
+ node.flags |= FLAG_DIRTY
404
+ if (batchDepth === 0) flush()
405
+ }
406
+ },
407
+
408
+ update(fn: (oldValue: T[]) => T[]) {
409
+ list.set(fn(list.get()))
410
+ },
411
+
412
+ at(index: number) {
413
+ return signals.get(keys[index])
414
+ },
415
+
416
+ keys() {
417
+ if (activeSink) {
418
+ if (!node.sinks && options?.watched)
419
+ node.stop = options.watched()
420
+ link(node, activeSink)
421
+ }
422
+ return keys.values()
423
+ },
424
+
425
+ byKey(key: string) {
426
+ return signals.get(key)
427
+ },
428
+
429
+ keyAt(index: number) {
430
+ return keys[index]
431
+ },
432
+
433
+ indexOfKey(key: string) {
434
+ return keys.indexOf(key)
435
+ },
436
+
437
+ add(value: T) {
438
+ const key = generateKey(value)
439
+ if (signals.has(key))
440
+ throw new DuplicateKeyError(TYPE_LIST, key, value)
441
+ if (!keys.includes(key)) keys.push(key)
442
+ validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
443
+ signals.set(key, createState(value))
444
+ node.sources = null
445
+ node.sourcesTail = null
446
+ propagate(node as unknown as SinkNode)
447
+ node.flags |= FLAG_DIRTY
448
+ if (batchDepth === 0) flush()
449
+ return key
450
+ },
451
+
452
+ remove(keyOrIndex: string | number) {
453
+ const key =
454
+ typeof keyOrIndex === 'number' ? keys[keyOrIndex] : keyOrIndex
455
+ const ok = signals.delete(key)
456
+ if (ok) {
457
+ const index =
458
+ typeof keyOrIndex === 'number'
459
+ ? keyOrIndex
460
+ : keys.indexOf(key)
461
+ if (index >= 0) keys.splice(index, 1)
462
+ node.sources = null
463
+ node.sourcesTail = null
464
+ propagate(node as unknown as SinkNode)
465
+ node.flags |= FLAG_DIRTY
466
+ if (batchDepth === 0) flush()
467
+ }
468
+ },
469
+
470
+ sort(compareFn?: (a: T, b: T) => number) {
471
+ const entries = keys
472
+ .map(key => [key, signals.get(key)?.get()] as [string, T])
473
+ .sort(
474
+ isFunction(compareFn)
475
+ ? (a, b) => compareFn(a[1], b[1])
476
+ : (a, b) => String(a[1]).localeCompare(String(b[1])),
477
+ )
478
+ const newOrder = entries.map(([key]) => key)
479
+
480
+ if (!keysEqual(keys, newOrder)) {
481
+ keys = newOrder
482
+ propagate(node as unknown as SinkNode)
483
+ node.flags |= FLAG_DIRTY
484
+ if (batchDepth === 0) flush()
485
+ }
486
+ },
487
+
488
+ splice(start: number, deleteCount?: number, ...items: T[]) {
489
+ const length = keys.length
490
+ const actualStart =
491
+ start < 0
492
+ ? Math.max(0, length + start)
493
+ : Math.min(start, length)
494
+ const actualDeleteCount = Math.max(
495
+ 0,
496
+ Math.min(
497
+ deleteCount ??
498
+ Math.max(0, length - Math.max(0, actualStart)),
499
+ length - actualStart,
500
+ ),
501
+ )
502
+
503
+ const add = {} as Record<string, T>
504
+ const remove = {} as Record<string, T>
505
+
506
+ // Collect items to delete
507
+ for (let i = 0; i < actualDeleteCount; i++) {
508
+ const index = actualStart + i
509
+ const key = keys[index]
510
+ if (key) {
511
+ const signal = signals.get(key)
512
+ if (signal) remove[key] = signal.get() as T
513
+ }
514
+ }
515
+
516
+ // Build new key order
517
+ const newOrder = keys.slice(0, actualStart)
518
+
519
+ for (const item of items) {
520
+ const key = generateKey(item)
521
+ if (signals.has(key) && !(key in remove))
522
+ throw new DuplicateKeyError(TYPE_LIST, key, item)
523
+ newOrder.push(key)
524
+ add[key] = item
525
+ }
526
+
527
+ newOrder.push(...keys.slice(actualStart + actualDeleteCount))
528
+
529
+ const changed = !!(
530
+ Object.keys(add).length || Object.keys(remove).length
531
+ )
532
+
533
+ if (changed) {
534
+ applyChanges({
535
+ add,
536
+ change: {},
537
+ remove,
538
+ changed,
539
+ })
540
+ keys = newOrder
541
+ propagate(node as unknown as SinkNode)
542
+ node.flags |= FLAG_DIRTY
543
+ if (batchDepth === 0) flush()
544
+ }
545
+
546
+ return Object.values(remove)
547
+ },
548
+
549
+ deriveCollection<R extends {}>(
550
+ cb: DeriveCollectionCallback<R, T>,
551
+ ): Collection<R> {
552
+ return (
553
+ deriveCollection as <T2 extends {}, U2 extends {}>(
554
+ source: CollectionSource<U2>,
555
+ callback: DeriveCollectionCallback<T2, U2>,
556
+ ) => Collection<T2>
557
+ )(list, cb)
558
+ },
559
+ }
560
+
561
+ return list
562
+ }
563
+
564
+ /**
565
+ * Checks if a value is a List signal.
566
+ *
567
+ * @since 0.15.0
568
+ * @param value - The value to check
569
+ * @returns True if the value is a List
570
+ */
571
+ function isList<T extends {}>(value: unknown): value is List<T> {
572
+ return isObjectOfType(value, TYPE_LIST)
573
+ }
574
+
575
+ /* === Exports === */
576
+
577
+ export {
578
+ type DiffResult,
579
+ type KeyConfig,
580
+ type List,
581
+ type ListOptions,
582
+ type UnknownRecord,
583
+ createList,
584
+ isEqual,
585
+ isList,
586
+ keysEqual,
587
+ TYPE_LIST,
588
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ validateCallback,
3
+ validateReadValue,
4
+ validateSignalValue,
5
+ } from '../errors'
6
+ import {
7
+ activeSink,
8
+ type ComputedOptions,
9
+ defaultEquals,
10
+ FLAG_DIRTY,
11
+ link,
12
+ type MemoCallback,
13
+ type MemoNode,
14
+ refresh,
15
+ type SinkNode,
16
+ TYPE_MEMO,
17
+ } from '../graph'
18
+ import { isObjectOfType, isSyncFunction } from '../util'
19
+
20
+ /* === Types === */
21
+
22
+ /**
23
+ * A derived reactive computation that caches its result.
24
+ * Automatically tracks dependencies and recomputes when they change.
25
+ *
26
+ * @template T - The type of value computed by the memo
27
+ */
28
+ type Memo<T extends {}> = {
29
+ readonly [Symbol.toStringTag]: 'Memo'
30
+
31
+ /**
32
+ * Gets the current value of the memo.
33
+ * Recomputes if dependencies have changed since last access.
34
+ * When called inside another reactive context, creates a dependency.
35
+ * @returns The computed value
36
+ * @throws UnsetSignalValueError If the memo value is still unset when read.
37
+ */
38
+ get(): T
39
+ }
40
+
41
+ /* === Exported Functions === */
42
+
43
+ /**
44
+ * Creates a derived reactive computation that caches its result.
45
+ * The computation automatically tracks dependencies and recomputes when they change.
46
+ * Uses lazy evaluation - only computes when the value is accessed.
47
+ *
48
+ * @since 0.18.0
49
+ * @template T - The type of value computed by the memo
50
+ * @param fn - The computation function that receives the previous value
51
+ * @param options - Optional configuration for the memo
52
+ * @returns A Memo object with a get() method
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const count = createState(0);
57
+ * const doubled = createMemo(() => count.get() * 2);
58
+ * console.log(doubled.get()); // 0
59
+ * count.set(5);
60
+ * console.log(doubled.get()); // 10
61
+ * ```
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * // Using previous value
66
+ * const sum = createMemo((prev) => prev + count.get(), { value: 0, equals: Object.is });
67
+ * ```
68
+ */
69
+ function createMemo<T extends {}>(
70
+ fn: (prev: T) => T,
71
+ options: ComputedOptions<T> & { value: T },
72
+ ): Memo<T>
73
+ function createMemo<T extends {}>(
74
+ fn: MemoCallback<T>,
75
+ options?: ComputedOptions<T>,
76
+ ): Memo<T>
77
+ function createMemo<T extends {}>(
78
+ fn: MemoCallback<T>,
79
+ options?: ComputedOptions<T>,
80
+ ): Memo<T> {
81
+ validateCallback(TYPE_MEMO, fn, isSyncFunction)
82
+ if (options?.value !== undefined)
83
+ validateSignalValue(TYPE_MEMO, options.value, options?.guard)
84
+
85
+ const node: MemoNode<T> = {
86
+ fn,
87
+ value: options?.value as T,
88
+ flags: FLAG_DIRTY,
89
+ sources: null,
90
+ sourcesTail: null,
91
+ sinks: null,
92
+ sinksTail: null,
93
+ equals: options?.equals ?? defaultEquals,
94
+ error: undefined,
95
+ }
96
+
97
+ return {
98
+ [Symbol.toStringTag]: TYPE_MEMO,
99
+ get() {
100
+ if (activeSink) link(node, activeSink)
101
+ refresh(node as unknown as SinkNode)
102
+ if (node.error) throw node.error
103
+ validateReadValue(TYPE_MEMO, node.value)
104
+ return node.value
105
+ },
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Checks if a value is a Memo signal.
111
+ *
112
+ * @since 0.18.0
113
+ * @param value - The value to check
114
+ * @returns True if the value is a Memo
115
+ */
116
+ function isMemo<T extends {} = unknown & {}>(value: unknown): value is Memo<T> {
117
+ return isObjectOfType(value, TYPE_MEMO)
118
+ }
119
+
120
+ export { createMemo, isMemo, type Memo }