@zeix/cause-effect 0.16.0 → 0.17.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 +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +33 -34
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -0
- package/archive/state.ts +89 -0
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +902 -501
- package/index.js +1 -1
- package/index.ts +42 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +28 -52
- package/src/effect.ts +9 -9
- package/src/errors.ts +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +72 -121
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +344 -1663
- package/types/index.d.ts +11 -9
- package/types/src/classes/collection.d.ts +32 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- package/src/state.ts +0 -98
- package/src/store.ts +0 -525
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -66
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isEqual } from '../diff'
|
|
2
|
+
import { validateCallback, validateSignalValue } from '../errors'
|
|
3
|
+
import { notifyWatchers, subscribeActiveWatcher, type Watcher } from '../system'
|
|
4
|
+
import { isObjectOfType, UNSET } from '../util'
|
|
5
|
+
|
|
6
|
+
/* === Constants === */
|
|
7
|
+
|
|
8
|
+
const TYPE_STATE = 'State' as const
|
|
9
|
+
|
|
10
|
+
/* === Class === */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new state signal.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.17.0
|
|
16
|
+
*/
|
|
17
|
+
class State<T extends {}> {
|
|
18
|
+
#watchers = new Set<Watcher>()
|
|
19
|
+
#value: T
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new state signal.
|
|
23
|
+
*
|
|
24
|
+
* @param {T} initialValue - Initial value of the state
|
|
25
|
+
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
26
|
+
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
27
|
+
*/
|
|
28
|
+
constructor(initialValue: T) {
|
|
29
|
+
validateSignalValue('state', initialValue)
|
|
30
|
+
|
|
31
|
+
this.#value = initialValue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get [Symbol.toStringTag](): string {
|
|
35
|
+
return TYPE_STATE
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the current value of the state signal.
|
|
40
|
+
*
|
|
41
|
+
* @returns {T} - Current value of the state
|
|
42
|
+
*/
|
|
43
|
+
get(): T {
|
|
44
|
+
subscribeActiveWatcher(this.#watchers)
|
|
45
|
+
return this.#value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set the value of the state signal.
|
|
50
|
+
*
|
|
51
|
+
* @param {T} newValue - New value of the state
|
|
52
|
+
* @returns {void}
|
|
53
|
+
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
54
|
+
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
55
|
+
*/
|
|
56
|
+
set(newValue: T): void {
|
|
57
|
+
validateSignalValue('state', newValue)
|
|
58
|
+
|
|
59
|
+
if (isEqual(this.#value, newValue)) return
|
|
60
|
+
this.#value = newValue
|
|
61
|
+
notifyWatchers(this.#watchers)
|
|
62
|
+
|
|
63
|
+
// Setting to UNSET clears the watchers so the signal can be garbage collected
|
|
64
|
+
if (UNSET === this.#value) this.#watchers.clear()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update the value of the state signal.
|
|
69
|
+
*
|
|
70
|
+
* @param {Function} updater - Function that takes the current value and returns the new value
|
|
71
|
+
* @returns {void}
|
|
72
|
+
* @throws {InvalidCallbackError} - If the updater function is not a function
|
|
73
|
+
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
74
|
+
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
75
|
+
*/
|
|
76
|
+
update(updater: (oldValue: T) => T): void {
|
|
77
|
+
validateCallback('state update', updater)
|
|
78
|
+
|
|
79
|
+
this.set(updater(this.#value))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* === Functions === */
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if the provided value is a State instance
|
|
87
|
+
*
|
|
88
|
+
* @since 0.9.0
|
|
89
|
+
* @param {unknown} value - Value to check
|
|
90
|
+
* @returns {boolean} - True if the value is a State instance, false otherwise
|
|
91
|
+
*/
|
|
92
|
+
const isState = /*#__PURE__*/ <T extends {}>(
|
|
93
|
+
value: unknown,
|
|
94
|
+
): value is State<T> => isObjectOfType(value, TYPE_STATE)
|
|
95
|
+
|
|
96
|
+
/* === Exports === */
|
|
97
|
+
|
|
98
|
+
export { TYPE_STATE, isState, State }
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { diff, type UnknownRecord } from '../diff'
|
|
2
|
+
import { DuplicateKeyError, validateSignalValue } from '../errors'
|
|
3
|
+
import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
|
|
4
|
+
import {
|
|
5
|
+
type Cleanup,
|
|
6
|
+
type Listener,
|
|
7
|
+
type Listeners,
|
|
8
|
+
notifyWatchers,
|
|
9
|
+
subscribeActiveWatcher,
|
|
10
|
+
type Watcher,
|
|
11
|
+
} from '../system'
|
|
12
|
+
import { isFunction, isObjectOfType, isRecord, isSymbol, UNSET } from '../util'
|
|
13
|
+
import { Composite } from './composite'
|
|
14
|
+
import type { List } from './list'
|
|
15
|
+
import type { State } from './state'
|
|
16
|
+
|
|
17
|
+
/* === Types === */
|
|
18
|
+
|
|
19
|
+
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
20
|
+
[K in keyof T]: T[K] extends readonly (infer U extends {})[]
|
|
21
|
+
? List<U>
|
|
22
|
+
: T[K] extends UnknownRecord
|
|
23
|
+
? Store<T[K]>
|
|
24
|
+
: State<T[K] & {}>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* === Constants === */
|
|
28
|
+
|
|
29
|
+
const TYPE_STORE = 'Store' as const
|
|
30
|
+
|
|
31
|
+
/* === Store Implementation === */
|
|
32
|
+
|
|
33
|
+
class BaseStore<T extends UnknownRecord> {
|
|
34
|
+
#composite: Composite<T, Signal<T[keyof T] & {}>>
|
|
35
|
+
#watchers = new Set<Watcher>()
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a new store with the given initial value.
|
|
39
|
+
*
|
|
40
|
+
* @param {T} initialValue - The initial value of the store
|
|
41
|
+
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
42
|
+
* @throws {InvalidSignalValueError} - If the initial value is not an object
|
|
43
|
+
*/
|
|
44
|
+
constructor(initialValue: T) {
|
|
45
|
+
validateSignalValue('store', initialValue, isRecord)
|
|
46
|
+
|
|
47
|
+
this.#composite = new Composite<T, Signal<T[keyof T] & {}>>(
|
|
48
|
+
initialValue,
|
|
49
|
+
<K extends keyof T & string>(
|
|
50
|
+
key: K,
|
|
51
|
+
value: unknown,
|
|
52
|
+
): value is T[K] & {} => {
|
|
53
|
+
validateSignalValue(`store for key "${key}"`, value)
|
|
54
|
+
return true
|
|
55
|
+
},
|
|
56
|
+
value => createMutableSignal(value),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get #value(): T {
|
|
61
|
+
const record = {} as UnknownRecord
|
|
62
|
+
for (const [key, signal] of this.#composite.signals.entries())
|
|
63
|
+
record[key] = signal.get()
|
|
64
|
+
return record as T
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Public methods
|
|
68
|
+
get [Symbol.toStringTag](): 'Store' {
|
|
69
|
+
return TYPE_STORE
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get [Symbol.isConcatSpreadable](): boolean {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
*[Symbol.iterator](): IterableIterator<
|
|
77
|
+
[string, MutableSignal<T[keyof T] & {}>]
|
|
78
|
+
> {
|
|
79
|
+
for (const [key, signal] of this.#composite.signals.entries())
|
|
80
|
+
yield [key, signal as MutableSignal<T[keyof T] & {}>]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get(): T {
|
|
84
|
+
subscribeActiveWatcher(this.#watchers)
|
|
85
|
+
return this.#value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
set(newValue: T): void {
|
|
89
|
+
if (UNSET === newValue) {
|
|
90
|
+
this.#composite.clear()
|
|
91
|
+
notifyWatchers(this.#watchers)
|
|
92
|
+
this.#watchers.clear()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const oldValue = this.#value
|
|
97
|
+
const changed = this.#composite.change(diff(oldValue, newValue))
|
|
98
|
+
if (changed) notifyWatchers(this.#watchers)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
keys(): IterableIterator<string> {
|
|
102
|
+
return this.#composite.signals.keys()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
byKey<K extends keyof T & string>(
|
|
106
|
+
key: K,
|
|
107
|
+
): T[K] extends readonly (infer U extends {})[]
|
|
108
|
+
? List<U>
|
|
109
|
+
: T[K] extends UnknownRecord
|
|
110
|
+
? Store<T[K]>
|
|
111
|
+
: T[K] extends unknown & {}
|
|
112
|
+
? State<T[K] & {}>
|
|
113
|
+
: State<T[K] & {}> | undefined {
|
|
114
|
+
return this.#composite.signals.get(
|
|
115
|
+
key,
|
|
116
|
+
) as T[K] extends readonly (infer U extends {})[]
|
|
117
|
+
? List<U>
|
|
118
|
+
: T[K] extends UnknownRecord
|
|
119
|
+
? Store<T[K]>
|
|
120
|
+
: T[K] extends unknown & {}
|
|
121
|
+
? State<T[K] & {}>
|
|
122
|
+
: State<T[K] & {}> | undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
update(fn: (oldValue: T) => T): void {
|
|
126
|
+
this.set(fn(this.get()))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
130
|
+
if (this.#composite.signals.has(key))
|
|
131
|
+
throw new DuplicateKeyError('store', key, value)
|
|
132
|
+
|
|
133
|
+
const ok = this.#composite.add(key, value)
|
|
134
|
+
if (ok) notifyWatchers(this.#watchers)
|
|
135
|
+
return key
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
remove(key: string): void {
|
|
139
|
+
const ok = this.#composite.remove(key)
|
|
140
|
+
if (ok) notifyWatchers(this.#watchers)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
on<K extends keyof Omit<Listeners, 'sort'>>(
|
|
144
|
+
type: K,
|
|
145
|
+
listener: Listener<K>,
|
|
146
|
+
): Cleanup {
|
|
147
|
+
return this.#composite.on(type, listener)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* === Functions === */
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a new store with deeply nested reactive properties
|
|
155
|
+
*
|
|
156
|
+
* @since 0.15.0
|
|
157
|
+
* @param {T} initialValue - Initial object or array value of the store
|
|
158
|
+
* @returns {Store<T>} - New store with reactive properties that preserves the original type T
|
|
159
|
+
*/
|
|
160
|
+
const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
161
|
+
const instance = new BaseStore(initialValue)
|
|
162
|
+
|
|
163
|
+
// Return proxy for property access
|
|
164
|
+
return new Proxy(instance, {
|
|
165
|
+
get(target, prop) {
|
|
166
|
+
if (prop in target) {
|
|
167
|
+
const value = Reflect.get(target, prop)
|
|
168
|
+
return isFunction(value) ? value.bind(target) : value
|
|
169
|
+
}
|
|
170
|
+
if (!isSymbol(prop)) return target.byKey(prop)
|
|
171
|
+
},
|
|
172
|
+
has(target, prop) {
|
|
173
|
+
if (prop in target) return true
|
|
174
|
+
return target.byKey(String(prop)) !== undefined
|
|
175
|
+
},
|
|
176
|
+
ownKeys(target) {
|
|
177
|
+
return Array.from(target.keys())
|
|
178
|
+
},
|
|
179
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
180
|
+
if (prop in target)
|
|
181
|
+
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
182
|
+
if (isSymbol(prop)) return undefined
|
|
183
|
+
|
|
184
|
+
const signal = target.byKey(String(prop))
|
|
185
|
+
return signal
|
|
186
|
+
? {
|
|
187
|
+
enumerable: true,
|
|
188
|
+
configurable: true,
|
|
189
|
+
writable: true,
|
|
190
|
+
value: signal,
|
|
191
|
+
}
|
|
192
|
+
: undefined
|
|
193
|
+
},
|
|
194
|
+
}) as Store<T>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if the provided value is a Store instance
|
|
199
|
+
*
|
|
200
|
+
* @since 0.15.0
|
|
201
|
+
* @param {unknown} value - Value to check
|
|
202
|
+
* @returns {boolean} - True if the value is a Store instance, false otherwise
|
|
203
|
+
*/
|
|
204
|
+
const isStore = <T extends UnknownRecord>(
|
|
205
|
+
value: unknown,
|
|
206
|
+
): value is BaseStore<T> => isObjectOfType(value, TYPE_STORE)
|
|
207
|
+
|
|
208
|
+
/* === Exports === */
|
|
209
|
+
|
|
210
|
+
export { createStore, isStore, BaseStore, TYPE_STORE, type Store }
|
package/src/diff.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { CircularDependencyError } from './errors'
|
|
2
|
-
import { isRecord, isRecordOrArray, UNSET } from './util'
|
|
2
|
+
import { isNonNullObject, isRecord, isRecordOrArray, UNSET } from './util'
|
|
3
3
|
|
|
4
4
|
/* === Types === */
|
|
5
5
|
|
|
6
|
-
type UnknownRecord = Record<string, unknown
|
|
6
|
+
type UnknownRecord = Record<string, unknown>
|
|
7
7
|
type UnknownArray = ReadonlyArray<unknown & {}>
|
|
8
|
-
type ArrayToRecord<T extends UnknownArray> = {
|
|
9
|
-
[key: string]: T extends Array<infer U extends {}> ? U : never
|
|
10
|
-
}
|
|
11
|
-
type UnknownRecordOrArray = UnknownRecord | ArrayToRecord<UnknownArray>
|
|
12
8
|
|
|
13
|
-
type DiffResult
|
|
9
|
+
type DiffResult = {
|
|
14
10
|
changed: boolean
|
|
15
|
-
add:
|
|
16
|
-
change:
|
|
17
|
-
remove:
|
|
11
|
+
add: UnknownRecord
|
|
12
|
+
change: UnknownRecord
|
|
13
|
+
remove: UnknownRecord
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
/* === Functions === */
|
|
@@ -32,14 +28,14 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
32
28
|
// Fast paths
|
|
33
29
|
if (Object.is(a, b)) return true
|
|
34
30
|
if (typeof a !== typeof b) return false
|
|
35
|
-
if (
|
|
31
|
+
if (!isNonNullObject(a) || !isNonNullObject(b)) return false
|
|
36
32
|
|
|
37
33
|
// Cycle detection
|
|
38
34
|
if (!visited) visited = new WeakSet()
|
|
39
35
|
if (visited.has(a as object) || visited.has(b as object))
|
|
40
36
|
throw new CircularDependencyError('isEqual')
|
|
41
|
-
visited.add(a
|
|
42
|
-
visited.add(b
|
|
37
|
+
visited.add(a)
|
|
38
|
+
visited.add(b)
|
|
43
39
|
|
|
44
40
|
try {
|
|
45
41
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -59,14 +55,7 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
59
55
|
if (aKeys.length !== bKeys.length) return false
|
|
60
56
|
for (const key of aKeys) {
|
|
61
57
|
if (!(key in b)) return false
|
|
62
|
-
if (
|
|
63
|
-
!isEqual(
|
|
64
|
-
(a as Record<string, unknown>)[key],
|
|
65
|
-
(b as Record<string, unknown>)[key],
|
|
66
|
-
visited,
|
|
67
|
-
)
|
|
68
|
-
)
|
|
69
|
-
return false
|
|
58
|
+
if (!isEqual(a[key], b[key], visited)) return false
|
|
70
59
|
}
|
|
71
60
|
return true
|
|
72
61
|
}
|
|
@@ -75,8 +64,8 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
75
64
|
// (which would have been caught by Object.is at the beginning)
|
|
76
65
|
return false
|
|
77
66
|
} finally {
|
|
78
|
-
visited.delete(a
|
|
79
|
-
visited.delete(b
|
|
67
|
+
visited.delete(a)
|
|
68
|
+
visited.delete(b)
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
71
|
|
|
@@ -86,12 +75,9 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
86
75
|
* @since 0.15.0
|
|
87
76
|
* @param {T} oldObj - The old record to compare
|
|
88
77
|
* @param {T} newObj - The new record to compare
|
|
89
|
-
* @returns {DiffResult
|
|
78
|
+
* @returns {DiffResult} The result of the comparison
|
|
90
79
|
*/
|
|
91
|
-
const diff = <T extends
|
|
92
|
-
oldObj: T,
|
|
93
|
-
newObj: T,
|
|
94
|
-
): DiffResult<T> => {
|
|
80
|
+
const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
|
|
95
81
|
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
96
82
|
const oldValid = isRecordOrArray(oldObj)
|
|
97
83
|
const newValid = isRecordOrArray(newObj)
|
|
@@ -108,9 +94,9 @@ const diff = <T extends UnknownRecordOrArray>(
|
|
|
108
94
|
|
|
109
95
|
const visited = new WeakSet()
|
|
110
96
|
|
|
111
|
-
const add
|
|
112
|
-
const change
|
|
113
|
-
const remove
|
|
97
|
+
const add = {} as UnknownRecord
|
|
98
|
+
const change = {} as UnknownRecord
|
|
99
|
+
const remove = {} as UnknownRecord
|
|
114
100
|
|
|
115
101
|
const oldKeys = Object.keys(oldObj)
|
|
116
102
|
const newKeys = Object.keys(newObj)
|
|
@@ -121,41 +107,31 @@ const diff = <T extends UnknownRecordOrArray>(
|
|
|
121
107
|
const newHas = key in newObj
|
|
122
108
|
|
|
123
109
|
if (!oldHas && newHas) {
|
|
124
|
-
add[key
|
|
110
|
+
add[key] = newObj[key]
|
|
125
111
|
continue
|
|
126
112
|
} else if (oldHas && !newHas) {
|
|
127
|
-
remove[key
|
|
113
|
+
remove[key] = UNSET
|
|
128
114
|
continue
|
|
129
115
|
}
|
|
130
116
|
|
|
131
|
-
const oldValue = oldObj[key]
|
|
132
|
-
const newValue = newObj[key]
|
|
117
|
+
const oldValue = oldObj[key]
|
|
118
|
+
const newValue = newObj[key]
|
|
133
119
|
|
|
134
|
-
if (!isEqual(oldValue, newValue, visited))
|
|
135
|
-
change[key as keyof T] = newValue
|
|
120
|
+
if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
|
|
136
121
|
}
|
|
137
122
|
|
|
138
|
-
const changed =
|
|
139
|
-
Object.keys(add).length > 0 ||
|
|
140
|
-
Object.keys(change).length > 0 ||
|
|
141
|
-
Object.keys(remove).length > 0
|
|
142
|
-
|
|
143
123
|
return {
|
|
144
|
-
changed,
|
|
145
124
|
add,
|
|
146
125
|
change,
|
|
147
126
|
remove,
|
|
127
|
+
changed: !!(
|
|
128
|
+
Object.keys(add).length ||
|
|
129
|
+
Object.keys(change).length ||
|
|
130
|
+
Object.keys(remove).length
|
|
131
|
+
),
|
|
148
132
|
}
|
|
149
133
|
}
|
|
150
134
|
|
|
151
135
|
/* === Exports === */
|
|
152
136
|
|
|
153
|
-
export {
|
|
154
|
-
type ArrayToRecord,
|
|
155
|
-
type DiffResult,
|
|
156
|
-
diff,
|
|
157
|
-
isEqual,
|
|
158
|
-
type UnknownRecord,
|
|
159
|
-
type UnknownArray,
|
|
160
|
-
type UnknownRecordOrArray,
|
|
161
|
-
}
|
|
137
|
+
export { type DiffResult, diff, isEqual, type UnknownRecord, type UnknownArray }
|
package/src/effect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
-
import { type Cleanup, createWatcher,
|
|
3
|
-
import { isAbortError, isAsyncFunction, isFunction
|
|
2
|
+
import { type Cleanup, createWatcher, trackSignalReads } from './system'
|
|
3
|
+
import { isAbortError, isAsyncFunction, isFunction } from './util'
|
|
4
4
|
|
|
5
5
|
/* === Types === */
|
|
6
6
|
|
|
@@ -26,14 +26,14 @@ type EffectCallback =
|
|
|
26
26
|
*/
|
|
27
27
|
const createEffect = (callback: EffectCallback): Cleanup => {
|
|
28
28
|
if (!isFunction(callback) || callback.length > 1)
|
|
29
|
-
throw new InvalidCallbackError('effect',
|
|
29
|
+
throw new InvalidCallbackError('effect', callback)
|
|
30
30
|
|
|
31
31
|
const isAsync = isAsyncFunction(callback)
|
|
32
32
|
let running = false
|
|
33
33
|
let controller: AbortController | undefined
|
|
34
34
|
|
|
35
35
|
const watcher = createWatcher(() =>
|
|
36
|
-
|
|
36
|
+
trackSignalReads(watcher, () => {
|
|
37
37
|
if (running) throw new CircularDependencyError('effect')
|
|
38
38
|
running = true
|
|
39
39
|
|
|
@@ -55,15 +55,15 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
55
55
|
isFunction(cleanup) &&
|
|
56
56
|
controller === currentController
|
|
57
57
|
)
|
|
58
|
-
watcher.
|
|
58
|
+
watcher.onCleanup(cleanup)
|
|
59
59
|
})
|
|
60
60
|
.catch(error => {
|
|
61
61
|
if (!isAbortError(error))
|
|
62
62
|
console.error('Async effect error:', error)
|
|
63
63
|
})
|
|
64
64
|
} else {
|
|
65
|
-
cleanup =
|
|
66
|
-
if (isFunction(cleanup)) watcher.
|
|
65
|
+
cleanup = callback()
|
|
66
|
+
if (isFunction(cleanup)) watcher.onCleanup(cleanup)
|
|
67
67
|
}
|
|
68
68
|
} catch (error) {
|
|
69
69
|
if (!isAbortError(error))
|
|
@@ -71,13 +71,13 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
running = false
|
|
74
|
-
}
|
|
74
|
+
}),
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
watcher()
|
|
78
78
|
return () => {
|
|
79
79
|
controller?.abort()
|
|
80
|
-
watcher.
|
|
80
|
+
watcher.stop()
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isMutableSignal, type MutableSignal } from './signal'
|
|
2
|
+
import { isFunction, isSymbol, UNSET, valueString } from './util'
|
|
3
|
+
|
|
1
4
|
class CircularDependencyError extends Error {
|
|
2
5
|
constructor(where: string) {
|
|
3
6
|
super(`Circular dependency detected in ${where}`)
|
|
@@ -5,16 +8,27 @@ class CircularDependencyError extends Error {
|
|
|
5
8
|
}
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
class DuplicateKeyError extends Error {
|
|
12
|
+
constructor(where: string, key: string, value?: unknown) {
|
|
13
|
+
super(
|
|
14
|
+
`Could not add ${where} key "${key}"${
|
|
15
|
+
value ? ` with value ${valueString(value)}` : ''
|
|
16
|
+
} because it already exists`,
|
|
17
|
+
)
|
|
18
|
+
this.name = 'DuplicateKeyError'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
class InvalidCallbackError extends TypeError {
|
|
9
|
-
constructor(where: string, value:
|
|
10
|
-
super(`Invalid ${where} callback ${value}`)
|
|
23
|
+
constructor(where: string, value: unknown) {
|
|
24
|
+
super(`Invalid ${where} callback ${valueString(value)}`)
|
|
11
25
|
this.name = 'InvalidCallbackError'
|
|
12
26
|
}
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
class InvalidSignalValueError extends TypeError {
|
|
16
|
-
constructor(where: string, value:
|
|
17
|
-
super(`Invalid signal value ${value} in ${where}`)
|
|
30
|
+
constructor(where: string, value: unknown) {
|
|
31
|
+
super(`Invalid signal value ${valueString(value)} in ${where}`)
|
|
18
32
|
this.name = 'InvalidSignalValueError'
|
|
19
33
|
}
|
|
20
34
|
}
|
|
@@ -26,39 +40,50 @@ class NullishSignalValueError extends TypeError {
|
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
class
|
|
30
|
-
constructor(
|
|
43
|
+
class ReadonlySignalError extends Error {
|
|
44
|
+
constructor(what: string, value: unknown) {
|
|
31
45
|
super(
|
|
32
|
-
`Could not
|
|
46
|
+
`Could not set ${what} to ${valueString(value)} because signal is read-only`,
|
|
33
47
|
)
|
|
34
|
-
this.name = '
|
|
48
|
+
this.name = 'ReadonlySignalError'
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
52
|
+
const validateCallback = (
|
|
53
|
+
where: string,
|
|
54
|
+
value: unknown,
|
|
55
|
+
guard: (value: unknown) => boolean = isFunction,
|
|
56
|
+
): void => {
|
|
57
|
+
if (!guard(value)) throw new InvalidCallbackError(where, value)
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
const validateSignalValue = (
|
|
61
|
+
where: string,
|
|
62
|
+
value: unknown,
|
|
63
|
+
guard: (value: unknown) => boolean = () =>
|
|
64
|
+
!(isSymbol(value) && value !== UNSET) || isFunction(value),
|
|
65
|
+
): void => {
|
|
66
|
+
if (value == null) throw new NullishSignalValueError(where)
|
|
67
|
+
if (!guard(value)) throw new InvalidSignalValueError(where, value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const guardMutableSignal = <T extends {}>(
|
|
71
|
+
what: string,
|
|
72
|
+
value: unknown,
|
|
73
|
+
signal: unknown,
|
|
74
|
+
): signal is MutableSignal<T> => {
|
|
75
|
+
if (!isMutableSignal(signal)) throw new ReadonlySignalError(what, value)
|
|
76
|
+
return true
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
export {
|
|
57
80
|
CircularDependencyError,
|
|
81
|
+
DuplicateKeyError,
|
|
58
82
|
InvalidCallbackError,
|
|
59
83
|
InvalidSignalValueError,
|
|
60
84
|
NullishSignalValueError,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
ReadonlySignalError,
|
|
86
|
+
validateCallback,
|
|
87
|
+
validateSignalValue,
|
|
88
|
+
guardMutableSignal,
|
|
64
89
|
}
|