@zeix/cause-effect 0.17.2 → 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 (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  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 -81
@@ -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,77 +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 MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
22
- ? List<U>
23
- : T extends UnknownRecord
24
- ? Store<T>
25
- : State<T>
26
- type ReadonlySignal<T extends {}> = Computed<T> // | Collection<T>
27
-
28
- type UnknownSignalRecord = Record<string, Signal<unknown & {}>>
29
-
30
- type SignalValues<S extends UnknownSignalRecord> = {
31
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
32
- }
33
-
34
- /* === Functions === */
30
+ /* === Factory Functions === */
35
31
 
36
32
  /**
37
- * Check whether a value is a Signal
33
+ * Create a derived signal from existing signals
38
34
  *
39
35
  * @since 0.9.0
40
- * @param {unknown} value - value to check
41
- * @returns {boolean} - true if value is a Signal, false otherwise
36
+ * @param callback - Computation callback function
37
+ * @param options - Optional configuration
42
38
  */
43
- const isSignal = /*#__PURE__*/ <T extends {}>(
44
- value: unknown,
45
- ): value is Signal<T> => isState(value) || isComputed(value) || isStore(value)
46
-
47
- /**
48
- * Check whether a value is a State, Store, or List
49
- *
50
- * @since 0.15.2
51
- * @param {unknown} value - Value to check
52
- * @returns {boolean} - True if value is a State, Store, or List, false otherwise
53
- */
54
- const isMutableSignal = /*#__PURE__*/ (
55
- value: unknown,
56
- ): value is MutableSignal<unknown & {}> =>
57
- 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
+ }
58
55
 
59
56
  /**
60
57
  * Convert a value to a Signal.
61
58
  *
62
59
  * @since 0.9.6
63
60
  */
61
+ function createSignal<T extends {}>(value: Signal<T>): Signal<T>
64
62
  function createSignal<T extends {}>(value: readonly T[]): List<T>
65
- function createSignal<T extends {}>(value: T[]): List<T>
66
63
  function createSignal<T extends UnknownRecord>(value: T): Store<T>
67
- 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>
68
66
  function createSignal<T extends {}>(value: T): State<T>
69
67
  function createSignal(value: unknown): unknown {
70
- if (isMemoCallback(value)) return new Memo(value)
71
- if (isTaskCallback(value)) return new Task(value)
72
- if (isUniformArray<unknown & {}>(value)) return new List(value)
73
- if (isRecord(value)) return createStore(value as UnknownRecord)
74
- 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 & {})
75
77
  }
76
78
 
77
79
  /**
@@ -79,26 +81,72 @@ function createSignal(value: unknown): unknown {
79
81
  *
80
82
  * @since 0.17.0
81
83
  */
84
+ function createMutableSignal<T extends {}>(
85
+ value: MutableSignal<T>,
86
+ ): MutableSignal<T>
82
87
  function createMutableSignal<T extends {}>(value: readonly T[]): List<T>
83
- function createMutableSignal<T extends {}>(value: T[]): List<T>
84
88
  function createMutableSignal<T extends UnknownRecord>(value: T): Store<T>
85
89
  function createMutableSignal<T extends {}>(value: T): State<T>
86
90
  function createMutableSignal(value: unknown): unknown {
87
- if (isUniformArray<unknown & {}>(value)) return new List(value)
88
- if (isRecord(value)) return createStore(value as UnknownRecord)
89
- 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)
90
110
  }
91
111
 
92
- /* === 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
+ }
93
143
 
94
144
  export {
95
- createMutableSignal,
145
+ type MutableSignal,
146
+ createComputed,
96
147
  createSignal,
97
- isMutableSignal,
148
+ createMutableSignal,
149
+ isComputed,
98
150
  isSignal,
99
- type MutableSignal,
100
- type ReadonlySignal,
101
- type Signal,
102
- type SignalValues,
103
- type UnknownSignalRecord,
151
+ isMutableSignal,
104
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,104 +1,131 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- batchSignalWrites,
3
+ batch,
4
4
  createEffect,
5
- Memo,
6
- match,
7
- resolve,
8
- State,
5
+ createList,
6
+ createMemo,
7
+ createState,
8
+ createStore,
9
9
  } from '../index.ts'
10
10
 
11
11
  /* === Tests === */
12
12
 
13
- describe('Batch', () => {
14
- test('should be triggered only once after repeated state change', () => {
15
- const cause = new State(0)
13
+ describe('batch', () => {
14
+ test('should trigger effect only once after repeated state changes', () => {
15
+ const source = createState(0)
16
16
  let result = 0
17
17
  let count = 0
18
18
  createEffect((): undefined => {
19
- result = cause.get()
19
+ result = source.get()
20
20
  count++
21
21
  })
22
- batchSignalWrites(() => {
23
- for (let i = 1; i <= 10; i++) cause.set(i)
22
+ expect(count).toBe(1)
23
+ batch(() => {
24
+ for (let i = 1; i <= 10; i++) source.set(i)
24
25
  })
25
26
  expect(result).toBe(10)
26
- expect(count).toBe(2) // + 1 for effect initialization
27
+ expect(count).toBe(2)
27
28
  })
28
29
 
29
- test('should be triggered only once when multiple signals are set', () => {
30
- const a = new State(3)
31
- const b = new State(4)
32
- const c = new State(5)
33
- 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())
34
35
  let result = 0
35
36
  let count = 0
36
- createEffect(() => {
37
- const resolved = resolve({ sum })
38
- match(resolved, {
39
- ok: ({ sum: res }) => {
40
- result = res
41
- count++
42
- },
43
- err: () => {},
44
- })
37
+ createEffect((): undefined => {
38
+ result = sum.get()
39
+ count++
45
40
  })
46
- batchSignalWrites(() => {
41
+ expect(result).toBe(12)
42
+ expect(count).toBe(1)
43
+ batch(() => {
47
44
  a.set(6)
48
45
  b.set(8)
49
46
  c.set(10)
50
47
  })
51
48
  expect(result).toBe(24)
52
- expect(count).toBe(2) // + 1 for effect initialization
49
+ expect(count).toBe(2)
53
50
  })
54
51
 
55
- test('should prove example from README works', () => {
56
- // State: define an array of Signal<number>
57
- const signals = [new State(2), new State(3), new State(5)]
58
-
59
- // Computed: derive a calculation ...
60
- const sum = new Memo(() => {
61
- const v = signals.reduce((total, v) => total + v.get(), 0)
62
- if (!Number.isFinite(v)) throw new Error('Invalid value')
63
- 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)
64
65
  })
66
+ expect(result).toBe('Bob (25)')
67
+ expect(count).toBe(2)
68
+ })
65
69
 
70
+ test('should batch list mutations', () => {
71
+ const list = createList([1, 2, 3])
66
72
  let result = 0
67
- let okCount = 0
68
- let errCount = 0
69
-
70
- // Effect: switch cases for the result
71
- createEffect(() => {
72
- const resolved = resolve({ sum })
73
- match(resolved, {
74
- ok: ({ sum: v }) => {
75
- result = v
76
- okCount++
77
- // console.log('Sum:', v)
78
- },
79
- err: () => {
80
- errCount++
81
- // console.error('Error:', error)
82
- },
83
- })
73
+ let count = 0
74
+ createEffect((): undefined => {
75
+ result = list.get().reduce((sum, v) => sum + v, 0)
76
+ count++
84
77
  })
85
-
86
- expect(okCount).toBe(1)
87
- expect(result).toBe(10)
88
-
89
- // Batch: apply changes to all signals in a single transaction
90
- batchSignalWrites(() => {
91
- signals.forEach(signal => signal.update(v => v * 2))
78
+ expect(result).toBe(6)
79
+ expect(count).toBe(1)
80
+ batch(() => {
81
+ list.add(4)
82
+ list.add(5)
92
83
  })
84
+ expect(result).toBe(15)
85
+ expect(count).toBe(2)
86
+ })
93
87
 
94
- expect(okCount).toBe(2)
95
- expect(result).toBe(20)
96
-
97
- // Provoke an error
98
- 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
+ })
99
109
 
100
- expect(errCount).toBe(1)
101
- expect(okCount).toBe(2) // should not have changed due to error
102
- 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)
103
130
  })
104
131
  })