@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.
- package/.ai-context.md +163 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- 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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
33
|
+
* Create a derived signal from existing signals
|
|
39
34
|
*
|
|
40
35
|
* @since 0.9.0
|
|
41
|
-
* @param
|
|
42
|
-
* @
|
|
36
|
+
* @param callback - Computation callback function
|
|
37
|
+
* @param options - Optional configuration
|
|
43
38
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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 (
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
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 (
|
|
89
|
-
if (
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
type MutableSignal,
|
|
146
|
+
createComputed,
|
|
97
147
|
createSignal,
|
|
98
|
-
|
|
148
|
+
createMutableSignal,
|
|
149
|
+
isComputed,
|
|
99
150
|
isSignal,
|
|
100
|
-
|
|
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
|
-
|
|
4
|
-
typeof
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
value
|
|
37
|
-
|
|
19
|
+
function isObjectOfType<T>(value: unknown, type: string): value is T {
|
|
20
|
+
return Object.prototype.toString.call(value) === `[object ${type}]`
|
|
21
|
+
}
|
|
38
22
|
|
|
39
|
-
|
|
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
|
|
25
|
+
): value is T {
|
|
26
|
+
return isObjectOfType(value, 'Object')
|
|
27
|
+
}
|
|
44
28
|
|
|
45
|
-
|
|
29
|
+
function isUniformArray<T>(
|
|
46
30
|
value: unknown,
|
|
47
|
-
guard
|
|
48
|
-
): value is T[]
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
}
|
package/test/batch.test.ts
CHANGED
|
@@ -1,97 +1,131 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
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('
|
|
7
|
-
test('should
|
|
8
|
-
const
|
|
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 =
|
|
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++)
|
|
24
|
+
for (let i = 1; i <= 10; i++) source.set(i)
|
|
17
25
|
})
|
|
18
26
|
expect(result).toBe(10)
|
|
19
|
-
expect(count).toBe(2)
|
|
27
|
+
expect(count).toBe(2)
|
|
20
28
|
})
|
|
21
29
|
|
|
22
|
-
test('should
|
|
23
|
-
const a =
|
|
24
|
-
const b =
|
|
25
|
-
const c =
|
|
26
|
-
const sum =
|
|
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
|
-
|
|
31
|
-
|
|
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)
|
|
49
|
+
expect(count).toBe(2)
|
|
46
50
|
})
|
|
47
51
|
|
|
48
|
-
test('should
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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
|
-
|
|
81
|
+
list.add(4)
|
|
82
|
+
list.add(5)
|
|
85
83
|
})
|
|
84
|
+
expect(result).toBe(15)
|
|
85
|
+
expect(count).toBe(2)
|
|
86
|
+
})
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
})
|