@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,146 @@
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
+ refresh,
13
+ type SinkNode,
14
+ type TaskCallback,
15
+ type TaskNode,
16
+ TYPE_TASK,
17
+ } from '../graph'
18
+ import { isAsyncFunction, isObjectOfType } from '../util'
19
+
20
+ /* === Types === */
21
+
22
+ /**
23
+ * An asynchronous reactive computation (colorless async).
24
+ * Automatically tracks dependencies and re-executes when they change.
25
+ * Provides abort semantics and pending state tracking.
26
+ *
27
+ * @template T - The type of value resolved by the task
28
+ */
29
+ type Task<T extends {}> = {
30
+ readonly [Symbol.toStringTag]: 'Task'
31
+
32
+ /**
33
+ * Gets the current value of the task.
34
+ * Returns the last resolved value, even while a new computation is pending.
35
+ * When called inside another reactive context, creates a dependency.
36
+ * @returns The current value
37
+ * @throws UnsetSignalValueError If the task value is still unset when read.
38
+ */
39
+ get(): T
40
+
41
+ /**
42
+ * Checks if the task is currently executing.
43
+ * @returns True if a computation is in progress
44
+ */
45
+ isPending(): boolean
46
+
47
+ /**
48
+ * Aborts the current computation if one is running.
49
+ * The task's AbortSignal will be triggered.
50
+ */
51
+ abort(): void
52
+ }
53
+
54
+ /* === Exported Functions === */
55
+
56
+ /**
57
+ * Creates an asynchronous reactive computation (colorless async).
58
+ * The computation automatically tracks dependencies and re-executes when they change.
59
+ * Provides abort semantics - in-flight computations are aborted when dependencies change.
60
+ *
61
+ * @since 0.18.0
62
+ * @template T - The type of value resolved by the task
63
+ * @param fn - The async computation function that receives the previous value and an AbortSignal
64
+ * @param options - Optional configuration for the task
65
+ * @returns A Task object with get(), isPending(), and abort() methods
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const userId = createState(1);
70
+ * const user = createTask(async (prev, signal) => {
71
+ * const response = await fetch(`/api/users/${userId.get()}`, { signal });
72
+ * return response.json();
73
+ * });
74
+ *
75
+ * // When userId changes, the previous fetch is aborted
76
+ * userId.set(2);
77
+ * ```
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Check pending state
82
+ * if (user.isPending()) {
83
+ * console.log('Loading...');
84
+ * }
85
+ * ```
86
+ */
87
+ function createTask<T extends {}>(
88
+ fn: (prev: T, signal: AbortSignal) => Promise<T>,
89
+ options: ComputedOptions<T> & { value: T },
90
+ ): Task<T>
91
+ function createTask<T extends {}>(
92
+ fn: TaskCallback<T>,
93
+ options?: ComputedOptions<T>,
94
+ ): Task<T>
95
+ function createTask<T extends {}>(
96
+ fn: TaskCallback<T>,
97
+ options?: ComputedOptions<T>,
98
+ ): Task<T> {
99
+ validateCallback(TYPE_TASK, fn, isAsyncFunction)
100
+ if (options?.value !== undefined)
101
+ validateSignalValue(TYPE_TASK, options.value, options?.guard)
102
+
103
+ const node: TaskNode<T> = {
104
+ fn,
105
+ value: options?.value as T,
106
+ sources: null,
107
+ sourcesTail: null,
108
+ sinks: null,
109
+ sinksTail: null,
110
+ flags: FLAG_DIRTY,
111
+ equals: options?.equals ?? defaultEquals,
112
+ controller: undefined,
113
+ error: undefined,
114
+ }
115
+
116
+ return {
117
+ [Symbol.toStringTag]: TYPE_TASK,
118
+ get(): T {
119
+ if (activeSink) link(node, activeSink)
120
+ refresh(node as unknown as SinkNode)
121
+ if (node.error) throw node.error
122
+ validateReadValue(TYPE_TASK, node.value)
123
+ return node.value
124
+ },
125
+ isPending(): boolean {
126
+ return !!node.controller
127
+ },
128
+ abort(): void {
129
+ node.controller?.abort()
130
+ node.controller = undefined
131
+ },
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Checks if a value is a Task signal.
137
+ *
138
+ * @since 0.18.0
139
+ * @param value - The value to check
140
+ * @returns True if the value is a Task
141
+ */
142
+ function isTask<T extends {} = unknown & {}>(value: unknown): value is Task<T> {
143
+ return isObjectOfType(value, TYPE_TASK)
144
+ }
145
+
146
+ export { createTask, isTask, type Task }
package/src/signal.ts CHANGED
@@ -1,78 +1,79 @@
1
+ import { InvalidSignalValueError } from './errors'
1
2
  import {
2
- type Computed,
3
- isComputed,
4
- isMemoCallback,
5
- isTaskCallback,
6
- Memo,
7
- Task,
8
- } from './classes/computed'
9
- import { isList, List } from './classes/list'
10
- import { isState, State } from './classes/state'
11
- import { createStore, isStore, type Store } from './classes/store'
12
- import type { UnknownRecord } from './diff'
13
- import { isRecord, isUniformArray } from './util'
3
+ type ComputedOptions,
4
+ type MemoCallback,
5
+ type Signal,
6
+ type TaskCallback,
7
+ TYPE_COLLECTION,
8
+ TYPE_LIST,
9
+ TYPE_MEMO,
10
+ TYPE_SENSOR,
11
+ TYPE_STATE,
12
+ TYPE_STORE,
13
+ TYPE_TASK,
14
+ } from './graph'
15
+ import { createList, isList, type List, type UnknownRecord } from './nodes/list'
16
+ import { createMemo, isMemo, type Memo } from './nodes/memo'
17
+ import { createState, isState, type State } from './nodes/state'
18
+ import { createStore, isStore, type Store } from './nodes/store'
19
+ import { createTask, isTask, type Task } from './nodes/task'
20
+ import { isAsyncFunction, isFunction, isRecord, isUniformArray } from './util'
14
21
 
15
22
  /* === Types === */
16
23
 
17
- type Signal<T extends {}> = {
24
+ type MutableSignal<T extends {}> = {
18
25
  get(): T
26
+ set(value: T): void
27
+ update(callback: (value: T) => T): void
19
28
  }
20
29
 
21
- type UnknownSignal = Signal<unknown & {}>
22
- type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
23
- ? List<U>
24
- : T extends UnknownRecord
25
- ? Store<T>
26
- : State<T>
27
- type ReadonlySignal<T extends {}> = Computed<T> // | Collection<T>
28
-
29
- type UnknownSignalRecord = Record<string, UnknownSignal>
30
-
31
- type SignalValues<S extends UnknownSignalRecord> = {
32
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
33
- }
34
-
35
- /* === Functions === */
30
+ /* === Factory Functions === */
36
31
 
37
32
  /**
38
- * Check whether a value is a Signal
33
+ * Create a derived signal from existing signals
39
34
  *
40
35
  * @since 0.9.0
41
- * @param {unknown} value - value to check
42
- * @returns {boolean} - true if value is a Signal, false otherwise
36
+ * @param callback - Computation callback function
37
+ * @param options - Optional configuration
43
38
  */
44
- const isSignal = /*#__PURE__*/ <T extends {}>(
45
- value: unknown,
46
- ): value is Signal<T> => isState(value) || isComputed(value) || isStore(value)
47
-
48
- /**
49
- * Check whether a value is a State, Store, or List
50
- *
51
- * @since 0.15.2
52
- * @param {unknown} value - Value to check
53
- * @returns {boolean} - True if value is a State, Store, or List, false otherwise
54
- */
55
- const isMutableSignal = /*#__PURE__*/ (
56
- value: unknown,
57
- ): value is MutableSignal<unknown & {}> =>
58
- isState(value) || isStore(value) || isList(value)
39
+ function createComputed<T extends {}>(
40
+ callback: TaskCallback<T>,
41
+ options?: ComputedOptions<T>,
42
+ ): Task<T>
43
+ function createComputed<T extends {}>(
44
+ callback: MemoCallback<T>,
45
+ options?: ComputedOptions<T>,
46
+ ): Memo<T>
47
+ function createComputed<T extends {}>(
48
+ callback: TaskCallback<T> | MemoCallback<T>,
49
+ options?: ComputedOptions<T>,
50
+ ): Memo<T> | Task<T> {
51
+ return isAsyncFunction(callback)
52
+ ? createTask(callback as TaskCallback<T>, options)
53
+ : createMemo(callback as MemoCallback<T>, options)
54
+ }
59
55
 
60
56
  /**
61
57
  * Convert a value to a Signal.
62
58
  *
63
59
  * @since 0.9.6
64
60
  */
61
+ function createSignal<T extends {}>(value: Signal<T>): Signal<T>
65
62
  function createSignal<T extends {}>(value: readonly T[]): List<T>
66
- function createSignal<T extends {}>(value: T[]): List<T>
67
63
  function createSignal<T extends UnknownRecord>(value: T): Store<T>
68
- function createSignal<T extends {}>(value: () => T): Computed<T>
64
+ function createSignal<T extends {}>(value: TaskCallback<T>): Task<T>
65
+ function createSignal<T extends {}>(value: MemoCallback<T>): Memo<T>
69
66
  function createSignal<T extends {}>(value: T): State<T>
70
67
  function createSignal(value: unknown): unknown {
71
- if (isMemoCallback(value)) return new Memo(value)
72
- if (isTaskCallback(value)) return new Task(value)
73
- if (isUniformArray<unknown & {}>(value)) return new List(value)
74
- if (isRecord(value)) return createStore(value as UnknownRecord)
75
- return new State(value as unknown & {})
68
+ if (isSignal(value)) return value
69
+ if (value == null) throw new InvalidSignalValueError('createSignal', value)
70
+ if (isAsyncFunction(value))
71
+ return createTask(value as TaskCallback<unknown & {}>)
72
+ if (isFunction(value))
73
+ return createMemo(value as MemoCallback<unknown & {}>)
74
+ if (isUniformArray<unknown & {}>(value)) return createList(value)
75
+ if (isRecord(value)) return createStore(value)
76
+ return createState(value as unknown & {})
76
77
  }
77
78
 
78
79
  /**
@@ -80,27 +81,72 @@ function createSignal(value: unknown): unknown {
80
81
  *
81
82
  * @since 0.17.0
82
83
  */
84
+ function createMutableSignal<T extends {}>(
85
+ value: MutableSignal<T>,
86
+ ): MutableSignal<T>
83
87
  function createMutableSignal<T extends {}>(value: readonly T[]): List<T>
84
- function createMutableSignal<T extends {}>(value: T[]): List<T>
85
88
  function createMutableSignal<T extends UnknownRecord>(value: T): Store<T>
86
89
  function createMutableSignal<T extends {}>(value: T): State<T>
87
90
  function createMutableSignal(value: unknown): unknown {
88
- if (isUniformArray<unknown & {}>(value)) return new List(value)
89
- if (isRecord(value)) return createStore(value as UnknownRecord)
90
- return new State(value as unknown & {})
91
+ if (isMutableSignal(value)) return value
92
+ if (value == null || isFunction(value) || isSignal(value))
93
+ throw new InvalidSignalValueError('createMutableSignal', value)
94
+ if (isUniformArray<unknown & {}>(value)) return createList(value)
95
+ if (isRecord(value)) return createStore(value)
96
+ return createState(value as unknown & {})
97
+ }
98
+
99
+ /* === Guards === */
100
+
101
+ /**
102
+ * Check if a value is a computed signal
103
+ *
104
+ * @since 0.9.0
105
+ * @param value - Value to check
106
+ * @returns True if value is a computed signal, false otherwise
107
+ */
108
+ function isComputed<T extends {}>(value: unknown): value is Memo<T> {
109
+ return isMemo(value) || isTask(value)
91
110
  }
92
111
 
93
- /* === Exports === */
112
+ /**
113
+ * Check whether a value is a Signal
114
+ *
115
+ * @since 0.9.0
116
+ * @param value - Value to check
117
+ * @returns True if value is a Signal, false otherwise
118
+ */
119
+ function isSignal<T extends {}>(value: unknown): value is Signal<T> {
120
+ const signalsTypes = [
121
+ TYPE_STATE,
122
+ TYPE_MEMO,
123
+ TYPE_TASK,
124
+ TYPE_SENSOR,
125
+ TYPE_LIST,
126
+ TYPE_COLLECTION,
127
+ TYPE_STORE,
128
+ ]
129
+ const typeStyle = Object.prototype.toString.call(value).slice(8, -1)
130
+ return signalsTypes.includes(typeStyle)
131
+ }
132
+
133
+ /**
134
+ * Check whether a value is a State, Store, or List
135
+ *
136
+ * @since 0.15.2
137
+ * @param value - Value to check
138
+ * @returns True if value is a State, Store, or List, false otherwise
139
+ */
140
+ function isMutableSignal(value: unknown): value is MutableSignal<unknown & {}> {
141
+ return isState(value) || isStore(value) || isList(value)
142
+ }
94
143
 
95
144
  export {
96
- createMutableSignal,
145
+ type MutableSignal,
146
+ createComputed,
97
147
  createSignal,
98
- isMutableSignal,
148
+ createMutableSignal,
149
+ isComputed,
99
150
  isSignal,
100
- type MutableSignal,
101
- type ReadonlySignal,
102
- type Signal,
103
- type SignalValues,
104
- type UnknownSignal,
105
- type UnknownSignalRecord,
151
+ isMutableSignal,
106
152
  }
package/src/util.ts CHANGED
@@ -1,85 +1,54 @@
1
1
  /* === Utility Functions === */
2
2
 
3
- const isString = /*#__PURE__*/ (value: unknown): value is string =>
4
- typeof value === 'string'
5
-
6
- const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
7
- typeof value === 'number'
8
-
9
- const isSymbol = /*#__PURE__*/ (value: unknown): value is symbol =>
10
- typeof value === 'symbol'
11
-
12
- const isFunction = /*#__PURE__*/ <T>(
13
- fn: unknown,
14
- ): fn is (...args: unknown[]) => T => typeof fn === 'function'
3
+ function isFunction<T>(fn: unknown): fn is (...args: unknown[]) => T {
4
+ return typeof fn === 'function'
5
+ }
15
6
 
16
- const isAsyncFunction = /*#__PURE__*/ <T>(
7
+ function isAsyncFunction<T>(
17
8
  fn: unknown,
18
- ): fn is (...args: unknown[]) => Promise<T> =>
19
- isFunction(fn) && fn.constructor.name === 'AsyncFunction'
9
+ ): fn is (...args: unknown[]) => Promise<T> {
10
+ return isFunction(fn) && fn.constructor.name === 'AsyncFunction'
11
+ }
20
12
 
21
- const isSyncFunction = /*#__PURE__*/ <T extends unknown & { then?: undefined }>(
13
+ function isSyncFunction<T extends unknown & { then?: undefined }>(
22
14
  fn: unknown,
23
- ): fn is (...args: unknown[]) => T =>
24
- isFunction(fn) && fn.constructor.name !== 'AsyncFunction'
25
-
26
- const isNonNullObject = /*#__PURE__*/ (
27
- value: unknown,
28
- ): value is NonNullable<object> => value != null && typeof value === 'object'
29
-
30
- const isObjectOfType = /*#__PURE__*/ <T>(
31
- value: unknown,
32
- type: string,
33
- ): value is T => Object.prototype.toString.call(value) === `[object ${type}]`
15
+ ): fn is (...args: unknown[]) => T {
16
+ return isFunction(fn) && fn.constructor.name !== 'AsyncFunction'
17
+ }
34
18
 
35
- const isRecord = /*#__PURE__*/ <T extends Record<string, unknown>>(
36
- value: unknown,
37
- ): value is T => isObjectOfType(value, 'Object')
19
+ function isObjectOfType<T>(value: unknown, type: string): value is T {
20
+ return Object.prototype.toString.call(value) === `[object ${type}]`
21
+ }
38
22
 
39
- const isRecordOrArray = /*#__PURE__*/ <
40
- T extends Record<string | number, unknown> | ReadonlyArray<unknown>,
41
- >(
23
+ function isRecord<T extends Record<string, unknown>>(
42
24
  value: unknown,
43
- ): value is T => isRecord(value) || Array.isArray(value)
25
+ ): value is T {
26
+ return isObjectOfType(value, 'Object')
27
+ }
44
28
 
45
- const isUniformArray = <T>(
29
+ function isUniformArray<T>(
46
30
  value: unknown,
47
- guard = (item: T): item is T & {} => item != null,
48
- ): value is T[] => Array.isArray(value) && value.every(guard)
49
-
50
- const hasMethod = /*#__PURE__*/ <
51
- T extends object & Record<string, (...args: unknown[]) => unknown>,
52
- >(
53
- obj: T,
54
- methodName: string,
55
- ): obj is T & Record<string, (...args: unknown[]) => unknown> =>
56
- methodName in obj && isFunction(obj[methodName])
57
-
58
- const isAbortError = /*#__PURE__*/ (error: unknown): boolean =>
59
- error instanceof DOMException && error.name === 'AbortError'
31
+ guard: (item: T) => item is T & {} = (item): item is T & {} => item != null,
32
+ ): value is T[] {
33
+ return Array.isArray(value) && value.every(guard)
34
+ }
60
35
 
61
- const valueString = /*#__PURE__*/ (value: unknown): string =>
62
- isString(value)
36
+ function valueString(value: unknown): string {
37
+ return typeof value === 'string'
63
38
  ? `"${value}"`
64
39
  : !!value && typeof value === 'object'
65
40
  ? JSON.stringify(value)
66
41
  : String(value)
42
+ }
67
43
 
68
44
  /* === Exports === */
69
45
 
70
46
  export {
71
- isString,
72
- isNumber,
73
- isSymbol,
74
47
  isFunction,
75
48
  isAsyncFunction,
76
49
  isSyncFunction,
77
- isNonNullObject,
78
50
  isObjectOfType,
79
51
  isRecord,
80
- isRecordOrArray,
81
52
  isUniformArray,
82
- hasMethod,
83
- isAbortError,
84
53
  valueString,
85
54
  }
@@ -1,97 +1,131 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { batch, createEffect, Memo, match, resolve, State } from '../index.ts'
2
+ import {
3
+ batch,
4
+ createEffect,
5
+ createList,
6
+ createMemo,
7
+ createState,
8
+ createStore,
9
+ } from '../index.ts'
3
10
 
4
11
  /* === Tests === */
5
12
 
6
- describe('Batch', () => {
7
- test('should be triggered only once after repeated state change', () => {
8
- const cause = new State(0)
13
+ describe('batch', () => {
14
+ test('should trigger effect only once after repeated state changes', () => {
15
+ const source = createState(0)
9
16
  let result = 0
10
17
  let count = 0
11
18
  createEffect((): undefined => {
12
- result = cause.get()
19
+ result = source.get()
13
20
  count++
14
21
  })
22
+ expect(count).toBe(1)
15
23
  batch(() => {
16
- for (let i = 1; i <= 10; i++) cause.set(i)
24
+ for (let i = 1; i <= 10; i++) source.set(i)
17
25
  })
18
26
  expect(result).toBe(10)
19
- expect(count).toBe(2) // + 1 for effect initialization
27
+ expect(count).toBe(2)
20
28
  })
21
29
 
22
- test('should be triggered only once when multiple signals are set', () => {
23
- const a = new State(3)
24
- const b = new State(4)
25
- const c = new State(5)
26
- const sum = new Memo(() => a.get() + b.get() + c.get())
30
+ test('should trigger effect only once when multiple states change', () => {
31
+ const a = createState(3)
32
+ const b = createState(4)
33
+ const c = createState(5)
34
+ const sum = createMemo(() => a.get() + b.get() + c.get())
27
35
  let result = 0
28
36
  let count = 0
29
- createEffect(() => {
30
- const resolved = resolve({ sum })
31
- match(resolved, {
32
- ok: ({ sum: res }) => {
33
- result = res
34
- count++
35
- },
36
- err: () => {},
37
- })
37
+ createEffect((): undefined => {
38
+ result = sum.get()
39
+ count++
38
40
  })
41
+ expect(result).toBe(12)
42
+ expect(count).toBe(1)
39
43
  batch(() => {
40
44
  a.set(6)
41
45
  b.set(8)
42
46
  c.set(10)
43
47
  })
44
48
  expect(result).toBe(24)
45
- expect(count).toBe(2) // + 1 for effect initialization
49
+ expect(count).toBe(2)
46
50
  })
47
51
 
48
- test('should prove example from README works', () => {
49
- // State: define an array of Signal<number>
50
- const signals = [new State(2), new State(3), new State(5)]
51
-
52
- // Computed: derive a calculation ...
53
- const sum = new Memo(() => {
54
- const v = signals.reduce((total, v) => total + v.get(), 0)
55
- if (!Number.isFinite(v)) throw new Error('Invalid value')
56
- return v
52
+ test('should batch store property updates', () => {
53
+ const user = createStore({ name: 'Alice', age: 30 })
54
+ let result = ''
55
+ let count = 0
56
+ createEffect((): undefined => {
57
+ result = `${user.name.get()} (${user.age.get()})`
58
+ count++
59
+ })
60
+ expect(result).toBe('Alice (30)')
61
+ expect(count).toBe(1)
62
+ batch(() => {
63
+ user.name.set('Bob')
64
+ user.age.set(25)
57
65
  })
66
+ expect(result).toBe('Bob (25)')
67
+ expect(count).toBe(2)
68
+ })
58
69
 
70
+ test('should batch list mutations', () => {
71
+ const list = createList([1, 2, 3])
59
72
  let result = 0
60
- let okCount = 0
61
- let errCount = 0
62
-
63
- // Effect: switch cases for the result
64
- createEffect(() => {
65
- const resolved = resolve({ sum })
66
- match(resolved, {
67
- ok: ({ sum: v }) => {
68
- result = v
69
- okCount++
70
- // console.log('Sum:', v)
71
- },
72
- err: () => {
73
- errCount++
74
- // console.error('Error:', error)
75
- },
76
- })
73
+ let count = 0
74
+ createEffect((): undefined => {
75
+ result = list.get().reduce((sum, v) => sum + v, 0)
76
+ count++
77
77
  })
78
-
79
- expect(okCount).toBe(1)
80
- expect(result).toBe(10)
81
-
82
- // Batch: apply changes to all signals in a single transaction
78
+ expect(result).toBe(6)
79
+ expect(count).toBe(1)
83
80
  batch(() => {
84
- signals.forEach(signal => signal.update(v => v * 2))
81
+ list.add(4)
82
+ list.add(5)
85
83
  })
84
+ expect(result).toBe(15)
85
+ expect(count).toBe(2)
86
+ })
86
87
 
87
- expect(okCount).toBe(2)
88
- expect(result).toBe(20)
89
-
90
- // Provoke an error
91
- signals[0].set(NaN)
88
+ test('should batch mixed signal type updates', () => {
89
+ const count = createState(1)
90
+ const items = createList([10, 20])
91
+ const config = createStore({ multiplier: 2 })
92
+ let result = 0
93
+ let runs = 0
94
+ createEffect((): undefined => {
95
+ const sum = items.get().reduce((s, v) => s + v, 0)
96
+ result = (count.get() + sum) * config.multiplier.get()
97
+ runs++
98
+ })
99
+ expect(result).toBe(62) // (1 + 30) * 2
100
+ expect(runs).toBe(1)
101
+ batch(() => {
102
+ count.set(5)
103
+ items.add(30)
104
+ config.multiplier.set(3)
105
+ })
106
+ expect(result).toBe(195) // (5 + 60) * 3
107
+ expect(runs).toBe(2)
108
+ })
92
109
 
93
- expect(errCount).toBe(1)
94
- expect(okCount).toBe(2) // should not have changed due to error
95
- expect(result).toBe(20) // should not have changed due to error
110
+ test('should support nested batches', () => {
111
+ const a = createState(1)
112
+ const b = createState(2)
113
+ let result = 0
114
+ let count = 0
115
+ createEffect((): undefined => {
116
+ result = a.get() + b.get()
117
+ count++
118
+ })
119
+ expect(count).toBe(1)
120
+ batch(() => {
121
+ a.set(10)
122
+ batch(() => {
123
+ b.set(20)
124
+ })
125
+ // inner batch should not flush yet
126
+ expect(count).toBe(1)
127
+ })
128
+ expect(result).toBe(30)
129
+ expect(count).toBe(2)
96
130
  })
97
131
  })