@zeix/cause-effect 0.16.1 → 0.17.1
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 +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- 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 +853 -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/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -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/ref.d.ts +39 -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 +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- 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 -65
|
@@ -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,24 +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
8
|
|
|
9
|
-
type
|
|
10
|
-
[key: string]: T extends Array<infer U extends {}> ? U : never
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type PartialRecord<T> = T extends UnknownArray
|
|
14
|
-
? Partial<ArrayToRecord<T>>
|
|
15
|
-
: Partial<T>
|
|
16
|
-
|
|
17
|
-
type DiffResult<T extends UnknownRecord | UnknownArray = UnknownRecord> = {
|
|
9
|
+
type DiffResult = {
|
|
18
10
|
changed: boolean
|
|
19
|
-
add:
|
|
20
|
-
change:
|
|
21
|
-
remove:
|
|
11
|
+
add: UnknownRecord
|
|
12
|
+
change: UnknownRecord
|
|
13
|
+
remove: UnknownRecord
|
|
22
14
|
}
|
|
23
15
|
|
|
24
16
|
/* === Functions === */
|
|
@@ -36,14 +28,14 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
36
28
|
// Fast paths
|
|
37
29
|
if (Object.is(a, b)) return true
|
|
38
30
|
if (typeof a !== typeof b) return false
|
|
39
|
-
if (
|
|
31
|
+
if (!isNonNullObject(a) || !isNonNullObject(b)) return false
|
|
40
32
|
|
|
41
33
|
// Cycle detection
|
|
42
34
|
if (!visited) visited = new WeakSet()
|
|
43
35
|
if (visited.has(a as object) || visited.has(b as object))
|
|
44
36
|
throw new CircularDependencyError('isEqual')
|
|
45
|
-
visited.add(a
|
|
46
|
-
visited.add(b
|
|
37
|
+
visited.add(a)
|
|
38
|
+
visited.add(b)
|
|
47
39
|
|
|
48
40
|
try {
|
|
49
41
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -63,14 +55,7 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
63
55
|
if (aKeys.length !== bKeys.length) return false
|
|
64
56
|
for (const key of aKeys) {
|
|
65
57
|
if (!(key in b)) return false
|
|
66
|
-
if (
|
|
67
|
-
!isEqual(
|
|
68
|
-
(a as Record<string, unknown>)[key],
|
|
69
|
-
(b as Record<string, unknown>)[key],
|
|
70
|
-
visited,
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
return false
|
|
58
|
+
if (!isEqual(a[key], b[key], visited)) return false
|
|
74
59
|
}
|
|
75
60
|
return true
|
|
76
61
|
}
|
|
@@ -79,8 +64,8 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
79
64
|
// (which would have been caught by Object.is at the beginning)
|
|
80
65
|
return false
|
|
81
66
|
} finally {
|
|
82
|
-
visited.delete(a
|
|
83
|
-
visited.delete(b
|
|
67
|
+
visited.delete(a)
|
|
68
|
+
visited.delete(b)
|
|
84
69
|
}
|
|
85
70
|
}
|
|
86
71
|
|
|
@@ -90,12 +75,9 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
90
75
|
* @since 0.15.0
|
|
91
76
|
* @param {T} oldObj - The old record to compare
|
|
92
77
|
* @param {T} newObj - The new record to compare
|
|
93
|
-
* @returns {DiffResult
|
|
78
|
+
* @returns {DiffResult} The result of the comparison
|
|
94
79
|
*/
|
|
95
|
-
const diff = <T extends UnknownRecord
|
|
96
|
-
oldObj: T extends UnknownArray ? ArrayToRecord<T> : T,
|
|
97
|
-
newObj: T extends UnknownArray ? ArrayToRecord<T> : T,
|
|
98
|
-
): DiffResult<T> => {
|
|
80
|
+
const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
|
|
99
81
|
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
100
82
|
const oldValid = isRecordOrArray(oldObj)
|
|
101
83
|
const newValid = isRecordOrArray(newObj)
|
|
@@ -104,17 +86,17 @@ const diff = <T extends UnknownRecord | UnknownArray>(
|
|
|
104
86
|
const changed = !Object.is(oldObj, newObj)
|
|
105
87
|
return {
|
|
106
88
|
changed,
|
|
107
|
-
add: changed && newValid ? newObj :
|
|
108
|
-
change: {}
|
|
109
|
-
remove: changed && oldValid ? oldObj :
|
|
89
|
+
add: changed && newValid ? newObj : {},
|
|
90
|
+
change: {},
|
|
91
|
+
remove: changed && oldValid ? oldObj : {},
|
|
110
92
|
}
|
|
111
93
|
}
|
|
112
94
|
|
|
113
95
|
const visited = new WeakSet()
|
|
114
96
|
|
|
115
|
-
const add = {} as
|
|
116
|
-
const change = {} as
|
|
117
|
-
const remove = {} as
|
|
97
|
+
const add = {} as UnknownRecord
|
|
98
|
+
const change = {} as UnknownRecord
|
|
99
|
+
const remove = {} as UnknownRecord
|
|
118
100
|
|
|
119
101
|
const oldKeys = Object.keys(oldObj)
|
|
120
102
|
const newKeys = Object.keys(newObj)
|
|
@@ -138,27 +120,18 @@ const diff = <T extends UnknownRecord | UnknownArray>(
|
|
|
138
120
|
if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
|
|
139
121
|
}
|
|
140
122
|
|
|
141
|
-
const changed =
|
|
142
|
-
Object.keys(add).length > 0 ||
|
|
143
|
-
Object.keys(change).length > 0 ||
|
|
144
|
-
Object.keys(remove).length > 0
|
|
145
|
-
|
|
146
123
|
return {
|
|
147
|
-
changed,
|
|
148
124
|
add,
|
|
149
125
|
change,
|
|
150
126
|
remove,
|
|
127
|
+
changed: !!(
|
|
128
|
+
Object.keys(add).length ||
|
|
129
|
+
Object.keys(change).length ||
|
|
130
|
+
Object.keys(remove).length
|
|
131
|
+
),
|
|
151
132
|
}
|
|
152
133
|
}
|
|
153
134
|
|
|
154
135
|
/* === Exports === */
|
|
155
136
|
|
|
156
|
-
export {
|
|
157
|
-
type ArrayToRecord,
|
|
158
|
-
type DiffResult,
|
|
159
|
-
diff,
|
|
160
|
-
isEqual,
|
|
161
|
-
type UnknownRecord,
|
|
162
|
-
type UnknownArray,
|
|
163
|
-
type PartialRecord,
|
|
164
|
-
}
|
|
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,12 @@
|
|
|
1
|
+
import { isMutableSignal, type MutableSignal } from './signal'
|
|
2
|
+
import { isFunction, isSymbol, UNSET, valueString } from './util'
|
|
3
|
+
|
|
4
|
+
/* === Types === */
|
|
5
|
+
|
|
6
|
+
type Guard<T> = (value: unknown) => value is T
|
|
7
|
+
|
|
8
|
+
/* === Classes === */
|
|
9
|
+
|
|
1
10
|
class CircularDependencyError extends Error {
|
|
2
11
|
constructor(where: string) {
|
|
3
12
|
super(`Circular dependency detected in ${where}`)
|
|
@@ -5,16 +14,34 @@ class CircularDependencyError extends Error {
|
|
|
5
14
|
}
|
|
6
15
|
}
|
|
7
16
|
|
|
17
|
+
class DuplicateKeyError extends Error {
|
|
18
|
+
constructor(where: string, key: string, value?: unknown) {
|
|
19
|
+
super(
|
|
20
|
+
`Could not add ${where} key "${key}"${
|
|
21
|
+
value ? ` with value ${valueString(value)}` : ''
|
|
22
|
+
} because it already exists`,
|
|
23
|
+
)
|
|
24
|
+
this.name = 'DuplicateKeyError'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
class InvalidCallbackError extends TypeError {
|
|
9
|
-
constructor(where: string, value:
|
|
10
|
-
super(`Invalid ${where} callback ${value}`)
|
|
29
|
+
constructor(where: string, value: unknown) {
|
|
30
|
+
super(`Invalid ${where} callback ${valueString(value)}`)
|
|
11
31
|
this.name = 'InvalidCallbackError'
|
|
12
32
|
}
|
|
13
33
|
}
|
|
14
34
|
|
|
35
|
+
class InvalidCollectionSourceError extends TypeError {
|
|
36
|
+
constructor(where: string, value: unknown) {
|
|
37
|
+
super(`Invalid ${where} source ${valueString(value)}`)
|
|
38
|
+
this.name = 'InvalidCollectionSourceError'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
class InvalidSignalValueError extends TypeError {
|
|
16
|
-
constructor(where: string, value:
|
|
17
|
-
super(`Invalid signal value ${value} in ${where}`)
|
|
43
|
+
constructor(where: string, value: unknown) {
|
|
44
|
+
super(`Invalid signal value ${valueString(value)} in ${where}`)
|
|
18
45
|
this.name = 'InvalidSignalValueError'
|
|
19
46
|
}
|
|
20
47
|
}
|
|
@@ -26,39 +53,58 @@ class NullishSignalValueError extends TypeError {
|
|
|
26
53
|
}
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
class
|
|
30
|
-
constructor(
|
|
56
|
+
class ReadonlySignalError extends Error {
|
|
57
|
+
constructor(what: string, value: unknown) {
|
|
31
58
|
super(
|
|
32
|
-
`Could not
|
|
59
|
+
`Could not set ${what} to ${valueString(value)} because signal is read-only`,
|
|
33
60
|
)
|
|
34
|
-
this.name = '
|
|
61
|
+
this.name = 'ReadonlySignalError'
|
|
35
62
|
}
|
|
36
63
|
}
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
/* === Functions === */
|
|
66
|
+
|
|
67
|
+
const createError = /*#__PURE__*/ (reason: unknown): Error =>
|
|
68
|
+
reason instanceof Error ? reason : Error(String(reason))
|
|
69
|
+
|
|
70
|
+
const validateCallback = (
|
|
71
|
+
where: string,
|
|
72
|
+
value: unknown,
|
|
73
|
+
guard: (value: unknown) => boolean = isFunction,
|
|
74
|
+
): void => {
|
|
75
|
+
if (!guard(value)) throw new InvalidCallbackError(where, value)
|
|
45
76
|
}
|
|
46
77
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
const validateSignalValue = (
|
|
79
|
+
where: string,
|
|
80
|
+
value: unknown,
|
|
81
|
+
guard: (value: unknown) => boolean = () =>
|
|
82
|
+
!(isSymbol(value) && value !== UNSET) || isFunction(value),
|
|
83
|
+
): void => {
|
|
84
|
+
if (value == null) throw new NullishSignalValueError(where)
|
|
85
|
+
if (!guard(value)) throw new InvalidSignalValueError(where, value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const guardMutableSignal = <T extends {}>(
|
|
89
|
+
what: string,
|
|
90
|
+
value: unknown,
|
|
91
|
+
signal: unknown,
|
|
92
|
+
): signal is MutableSignal<T> => {
|
|
93
|
+
if (!isMutableSignal(signal)) throw new ReadonlySignalError(what, value)
|
|
94
|
+
return true
|
|
54
95
|
}
|
|
55
96
|
|
|
56
97
|
export {
|
|
98
|
+
type Guard,
|
|
57
99
|
CircularDependencyError,
|
|
100
|
+
DuplicateKeyError,
|
|
58
101
|
InvalidCallbackError,
|
|
102
|
+
InvalidCollectionSourceError,
|
|
59
103
|
InvalidSignalValueError,
|
|
60
104
|
NullishSignalValueError,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
ReadonlySignalError,
|
|
106
|
+
createError,
|
|
107
|
+
validateCallback,
|
|
108
|
+
validateSignalValue,
|
|
109
|
+
guardMutableSignal,
|
|
64
110
|
}
|
package/src/match.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createError } from './errors'
|
|
1
2
|
import type { ResolveResult } from './resolve'
|
|
2
3
|
import type { SignalValues, UnknownSignalRecord } from './signal'
|
|
3
|
-
import { toError } from './util'
|
|
4
4
|
|
|
5
5
|
/* === Types === */
|
|
6
6
|
|
|
@@ -32,17 +32,10 @@ function match<S extends UnknownSignalRecord>(
|
|
|
32
32
|
if (result.pending) handlers.nil?.()
|
|
33
33
|
else if (result.errors) handlers.err?.(result.errors)
|
|
34
34
|
else if (result.ok) handlers.ok(result.values)
|
|
35
|
-
} catch (
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
handlers.err
|
|
39
|
-
(!result.errors || !result.errors.includes(toError(error)))
|
|
40
|
-
)
|
|
41
|
-
handlers.err(
|
|
42
|
-
result.errors
|
|
43
|
-
? [...result.errors, toError(error)]
|
|
44
|
-
: [toError(error)],
|
|
45
|
-
)
|
|
35
|
+
} catch (e) {
|
|
36
|
+
const error = createError(e)
|
|
37
|
+
if (handlers.err && (!result.errors || !result.errors.includes(error)))
|
|
38
|
+
handlers.err(result.errors ? [...result.errors, error] : [error])
|
|
46
39
|
else throw error
|
|
47
40
|
}
|
|
48
41
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { UnknownRecord } from './diff'
|
|
2
|
+
import { createError } from './errors'
|
|
2
3
|
import type { SignalValues, UnknownSignalRecord } from './signal'
|
|
3
|
-
import {
|
|
4
|
+
import { UNSET } from './util'
|
|
4
5
|
|
|
5
6
|
/* === Types === */
|
|
6
7
|
|
|
@@ -33,7 +34,7 @@ function resolve<S extends UnknownSignalRecord>(signals: S): ResolveResult<S> {
|
|
|
33
34
|
if (value === UNSET) pending = true
|
|
34
35
|
else values[key] = value
|
|
35
36
|
} catch (e) {
|
|
36
|
-
errors.push(
|
|
37
|
+
errors.push(createError(e))
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|