@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
package/src/classes/store.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { type DiffResult, diff, type UnknownRecord } from '../diff'
|
|
2
|
-
import {
|
|
3
|
-
DuplicateKeyError,
|
|
4
|
-
guardMutableSignal,
|
|
5
|
-
validateSignalValue,
|
|
6
|
-
} from '../errors'
|
|
7
|
-
import { createMutableSignal, type MutableSignal } from '../signal'
|
|
8
|
-
import {
|
|
9
|
-
batch,
|
|
10
|
-
notifyOf,
|
|
11
|
-
registerWatchCallbacks,
|
|
12
|
-
type SignalOptions,
|
|
13
|
-
subscribeTo,
|
|
14
|
-
UNSET,
|
|
15
|
-
unsubscribeAllFrom,
|
|
16
|
-
} from '../system'
|
|
17
|
-
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
|
|
18
|
-
import type { List } from './list'
|
|
19
|
-
import type { State } from './state'
|
|
20
|
-
|
|
21
|
-
/* === Types === */
|
|
22
|
-
|
|
23
|
-
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
24
|
-
[K in keyof T]: T[K] extends readonly (infer U extends {})[]
|
|
25
|
-
? List<U>
|
|
26
|
-
: T[K] extends UnknownRecord
|
|
27
|
-
? Store<T[K]>
|
|
28
|
-
: State<T[K] & {}>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* === Constants === */
|
|
32
|
-
|
|
33
|
-
const TYPE_STORE = 'Store' as const
|
|
34
|
-
|
|
35
|
-
/* === Store Implementation === */
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Create a new store with the given initial value.
|
|
39
|
-
*
|
|
40
|
-
* @since 0.17.0
|
|
41
|
-
* @param {T} initialValue - The initial value of the store
|
|
42
|
-
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
43
|
-
* @throws {InvalidSignalValueError} - If the initial value is not an object
|
|
44
|
-
*/
|
|
45
|
-
class BaseStore<T extends UnknownRecord> {
|
|
46
|
-
#signals = new Map<keyof T & string, MutableSignal<T[keyof T] & {}>>()
|
|
47
|
-
|
|
48
|
-
constructor(initialValue: T, options?: SignalOptions<T>) {
|
|
49
|
-
validateSignalValue(
|
|
50
|
-
TYPE_STORE,
|
|
51
|
-
initialValue,
|
|
52
|
-
options?.guard ?? isRecord,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
this.#change({
|
|
56
|
-
add: initialValue,
|
|
57
|
-
change: {},
|
|
58
|
-
remove: {},
|
|
59
|
-
changed: true,
|
|
60
|
-
})
|
|
61
|
-
if (options?.watched)
|
|
62
|
-
registerWatchCallbacks(this, options.watched, options.unwatched)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get #value(): T {
|
|
66
|
-
const record = {} as UnknownRecord
|
|
67
|
-
for (const [key, signal] of this.#signals.entries())
|
|
68
|
-
record[key] = signal.get()
|
|
69
|
-
return record as T
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
#validate<K extends keyof T & string>(
|
|
73
|
-
key: K,
|
|
74
|
-
value: unknown,
|
|
75
|
-
): value is T[K] & {} {
|
|
76
|
-
validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
#add<K extends keyof T & string>(key: K, value: T[K]): boolean {
|
|
81
|
-
if (!this.#validate(key, value)) return false
|
|
82
|
-
|
|
83
|
-
this.#signals.set(
|
|
84
|
-
key,
|
|
85
|
-
createMutableSignal(value) as unknown as MutableSignal<
|
|
86
|
-
T[keyof T] & {}
|
|
87
|
-
>,
|
|
88
|
-
)
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
#change(changes: DiffResult): boolean {
|
|
93
|
-
// Additions
|
|
94
|
-
if (Object.keys(changes.add).length) {
|
|
95
|
-
for (const key in changes.add)
|
|
96
|
-
this.#add(
|
|
97
|
-
key,
|
|
98
|
-
changes.add[key] as T[Extract<keyof T, string>] & {},
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Changes
|
|
103
|
-
if (Object.keys(changes.change).length) {
|
|
104
|
-
batch(() => {
|
|
105
|
-
for (const key in changes.change) {
|
|
106
|
-
const value = changes.change[key]
|
|
107
|
-
if (!this.#validate(key, value)) continue
|
|
108
|
-
|
|
109
|
-
const signal = this.#signals.get(key)
|
|
110
|
-
if (guardMutableSignal(`list item "${key}"`, value, signal))
|
|
111
|
-
signal.set(value)
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Removals
|
|
117
|
-
if (Object.keys(changes.remove).length) {
|
|
118
|
-
for (const key in changes.remove) this.remove(key)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return changes.changed
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Public methods
|
|
125
|
-
get [Symbol.toStringTag](): 'Store' {
|
|
126
|
-
return TYPE_STORE
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
get [Symbol.isConcatSpreadable](): boolean {
|
|
130
|
-
return false
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
*[Symbol.iterator](): IterableIterator<
|
|
134
|
-
[string, MutableSignal<T[keyof T] & {}>]
|
|
135
|
-
> {
|
|
136
|
-
for (const [key, signal] of this.#signals.entries()) yield [key, signal]
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
keys(): IterableIterator<string> {
|
|
140
|
-
subscribeTo(this)
|
|
141
|
-
return this.#signals.keys()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
byKey<K extends keyof T & string>(
|
|
145
|
-
key: K,
|
|
146
|
-
): T[K] extends readonly (infer U extends {})[]
|
|
147
|
-
? List<U>
|
|
148
|
-
: T[K] extends UnknownRecord
|
|
149
|
-
? Store<T[K]>
|
|
150
|
-
: T[K] extends unknown & {}
|
|
151
|
-
? State<T[K] & {}>
|
|
152
|
-
: State<T[K] & {}> | undefined {
|
|
153
|
-
return this.#signals.get(key) as T[K] extends readonly (infer U extends
|
|
154
|
-
{})[]
|
|
155
|
-
? List<U>
|
|
156
|
-
: T[K] extends UnknownRecord
|
|
157
|
-
? Store<T[K]>
|
|
158
|
-
: T[K] extends unknown & {}
|
|
159
|
-
? State<T[K] & {}>
|
|
160
|
-
: State<T[K] & {}> | undefined
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
get(): T {
|
|
164
|
-
subscribeTo(this)
|
|
165
|
-
return this.#value
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
set(newValue: T): void {
|
|
169
|
-
if (UNSET === newValue) {
|
|
170
|
-
this.#signals.clear()
|
|
171
|
-
notifyOf(this)
|
|
172
|
-
unsubscribeAllFrom(this)
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const changed = this.#change(diff(this.#value, newValue))
|
|
177
|
-
if (changed) notifyOf(this)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
update(fn: (oldValue: T) => T): void {
|
|
181
|
-
this.set(fn(this.get()))
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
185
|
-
if (this.#signals.has(key))
|
|
186
|
-
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
187
|
-
|
|
188
|
-
const ok = this.#add(key, value)
|
|
189
|
-
if (ok) notifyOf(this)
|
|
190
|
-
return key
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
remove(key: string): void {
|
|
194
|
-
const ok = this.#signals.delete(key)
|
|
195
|
-
if (ok) notifyOf(this)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/* === Functions === */
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Create a new store with deeply nested reactive properties
|
|
203
|
-
*
|
|
204
|
-
* @since 0.15.0
|
|
205
|
-
* @param {T} initialValue - Initial object or array value of the store
|
|
206
|
-
* @param {SignalOptions<T>} options - Options for the store
|
|
207
|
-
* @returns {Store<T>} - New store with reactive properties that preserves the original type T
|
|
208
|
-
*/
|
|
209
|
-
const createStore = <T extends UnknownRecord>(
|
|
210
|
-
initialValue: T,
|
|
211
|
-
options?: SignalOptions<T>,
|
|
212
|
-
): Store<T> => {
|
|
213
|
-
const instance = new BaseStore(initialValue, options)
|
|
214
|
-
|
|
215
|
-
// Return proxy for property access
|
|
216
|
-
return new Proxy(instance, {
|
|
217
|
-
get(target, prop) {
|
|
218
|
-
if (prop in target) {
|
|
219
|
-
const value = Reflect.get(target, prop)
|
|
220
|
-
return isFunction(value) ? value.bind(target) : value
|
|
221
|
-
}
|
|
222
|
-
if (!isSymbol(prop)) return target.byKey(prop)
|
|
223
|
-
},
|
|
224
|
-
has(target, prop) {
|
|
225
|
-
if (prop in target) return true
|
|
226
|
-
return target.byKey(String(prop)) !== undefined
|
|
227
|
-
},
|
|
228
|
-
ownKeys(target) {
|
|
229
|
-
return Array.from(target.keys())
|
|
230
|
-
},
|
|
231
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
232
|
-
if (prop in target)
|
|
233
|
-
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
234
|
-
if (isSymbol(prop)) return undefined
|
|
235
|
-
|
|
236
|
-
const signal = target.byKey(String(prop))
|
|
237
|
-
return signal
|
|
238
|
-
? {
|
|
239
|
-
enumerable: true,
|
|
240
|
-
configurable: true,
|
|
241
|
-
writable: true,
|
|
242
|
-
value: signal,
|
|
243
|
-
}
|
|
244
|
-
: undefined
|
|
245
|
-
},
|
|
246
|
-
}) as Store<T>
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Check if the provided value is a Store instance
|
|
251
|
-
*
|
|
252
|
-
* @since 0.15.0
|
|
253
|
-
* @param {unknown} value - Value to check
|
|
254
|
-
* @returns {boolean} - True if the value is a Store instance, false otherwise
|
|
255
|
-
*/
|
|
256
|
-
const isStore = <T extends UnknownRecord>(
|
|
257
|
-
value: unknown,
|
|
258
|
-
): value is BaseStore<T> => isObjectOfType(value, TYPE_STORE)
|
|
259
|
-
|
|
260
|
-
/* === Exports === */
|
|
261
|
-
|
|
262
|
-
export { createStore, isStore, BaseStore, TYPE_STORE, type Store }
|
package/src/diff.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { CircularDependencyError } from './errors'
|
|
2
|
-
import { UNSET } from './system'
|
|
3
|
-
import { isNonNullObject, isRecord, isRecordOrArray } from './util'
|
|
4
|
-
|
|
5
|
-
/* === Types === */
|
|
6
|
-
|
|
7
|
-
type UnknownRecord = Record<string, unknown>
|
|
8
|
-
type UnknownArray = ReadonlyArray<unknown & {}>
|
|
9
|
-
|
|
10
|
-
type DiffResult = {
|
|
11
|
-
changed: boolean
|
|
12
|
-
add: UnknownRecord
|
|
13
|
-
change: UnknownRecord
|
|
14
|
-
remove: UnknownRecord
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* === Functions === */
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Checks if two values are equal with cycle detection
|
|
21
|
-
*
|
|
22
|
-
* @since 0.15.0
|
|
23
|
-
* @param {T} a - First value to compare
|
|
24
|
-
* @param {T} b - Second value to compare
|
|
25
|
-
* @param {WeakSet<object>} visited - Set to track visited objects for cycle detection
|
|
26
|
-
* @returns {boolean} Whether the two values are equal
|
|
27
|
-
*/
|
|
28
|
-
const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
29
|
-
// Fast paths
|
|
30
|
-
if (Object.is(a, b)) return true
|
|
31
|
-
if (typeof a !== typeof b) return false
|
|
32
|
-
if (!isNonNullObject(a) || !isNonNullObject(b)) return false
|
|
33
|
-
|
|
34
|
-
// Cycle detection
|
|
35
|
-
if (!visited) visited = new WeakSet()
|
|
36
|
-
if (visited.has(a as object) || visited.has(b as object))
|
|
37
|
-
throw new CircularDependencyError('isEqual')
|
|
38
|
-
visited.add(a)
|
|
39
|
-
visited.add(b)
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
43
|
-
if (a.length !== b.length) return false
|
|
44
|
-
for (let i = 0; i < a.length; i++) {
|
|
45
|
-
if (!isEqual(a[i], b[i], visited)) return false
|
|
46
|
-
}
|
|
47
|
-
return true
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
51
|
-
|
|
52
|
-
if (isRecord(a) && isRecord(b)) {
|
|
53
|
-
const aKeys = Object.keys(a)
|
|
54
|
-
const bKeys = Object.keys(b)
|
|
55
|
-
|
|
56
|
-
if (aKeys.length !== bKeys.length) return false
|
|
57
|
-
for (const key of aKeys) {
|
|
58
|
-
if (!(key in b)) return false
|
|
59
|
-
if (!isEqual(a[key], b[key], visited)) return false
|
|
60
|
-
}
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// For non-records/non-arrays, they are only equal if they are the same reference
|
|
65
|
-
// (which would have been caught by Object.is at the beginning)
|
|
66
|
-
return false
|
|
67
|
-
} finally {
|
|
68
|
-
visited.delete(a)
|
|
69
|
-
visited.delete(b)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Compares two records and returns a result object containing the differences.
|
|
75
|
-
*
|
|
76
|
-
* @since 0.15.0
|
|
77
|
-
* @param {T} oldObj - The old record to compare
|
|
78
|
-
* @param {T} newObj - The new record to compare
|
|
79
|
-
* @returns {DiffResult} The result of the comparison
|
|
80
|
-
*/
|
|
81
|
-
const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
|
|
82
|
-
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
83
|
-
const oldValid = isRecordOrArray(oldObj)
|
|
84
|
-
const newValid = isRecordOrArray(newObj)
|
|
85
|
-
if (!oldValid || !newValid) {
|
|
86
|
-
// For non-objects or non-plain objects, treat as complete change if different
|
|
87
|
-
const changed = !Object.is(oldObj, newObj)
|
|
88
|
-
return {
|
|
89
|
-
changed,
|
|
90
|
-
add: changed && newValid ? newObj : {},
|
|
91
|
-
change: {},
|
|
92
|
-
remove: changed && oldValid ? oldObj : {},
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const visited = new WeakSet()
|
|
97
|
-
|
|
98
|
-
const add = {} as UnknownRecord
|
|
99
|
-
const change = {} as UnknownRecord
|
|
100
|
-
const remove = {} as UnknownRecord
|
|
101
|
-
|
|
102
|
-
const oldKeys = Object.keys(oldObj)
|
|
103
|
-
const newKeys = Object.keys(newObj)
|
|
104
|
-
const allKeys = new Set([...oldKeys, ...newKeys])
|
|
105
|
-
|
|
106
|
-
for (const key of allKeys) {
|
|
107
|
-
const oldHas = key in oldObj
|
|
108
|
-
const newHas = key in newObj
|
|
109
|
-
|
|
110
|
-
if (!oldHas && newHas) {
|
|
111
|
-
add[key] = newObj[key]
|
|
112
|
-
continue
|
|
113
|
-
} else if (oldHas && !newHas) {
|
|
114
|
-
remove[key] = UNSET
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const oldValue = oldObj[key]
|
|
119
|
-
const newValue = newObj[key]
|
|
120
|
-
|
|
121
|
-
if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
add,
|
|
126
|
-
change,
|
|
127
|
-
remove,
|
|
128
|
-
changed: !!(
|
|
129
|
-
Object.keys(add).length ||
|
|
130
|
-
Object.keys(change).length ||
|
|
131
|
-
Object.keys(remove).length
|
|
132
|
-
),
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* === Exports === */
|
|
137
|
-
|
|
138
|
-
export { type DiffResult, diff, isEqual, type UnknownRecord, type UnknownArray }
|
package/src/effect.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
-
import { type Cleanup, createWatcher, type MaybeCleanup } from './system'
|
|
3
|
-
import { isAbortError, isAsyncFunction, isFunction } from './util'
|
|
4
|
-
|
|
5
|
-
/* === Types === */
|
|
6
|
-
|
|
7
|
-
type EffectCallback =
|
|
8
|
-
| (() => MaybeCleanup)
|
|
9
|
-
| ((abort: AbortSignal) => Promise<MaybeCleanup>)
|
|
10
|
-
|
|
11
|
-
/* === Functions === */
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Define what happens when a reactive state changes
|
|
15
|
-
*
|
|
16
|
-
* The callback can be synchronous or asynchronous. Async callbacks receive
|
|
17
|
-
* an AbortSignal parameter, which is automatically aborted when the effect
|
|
18
|
-
* re-runs or is cleaned up, preventing stale async operations.
|
|
19
|
-
*
|
|
20
|
-
* @since 0.1.0
|
|
21
|
-
* @param {EffectCallback} callback - Synchronous or asynchronous effect callback
|
|
22
|
-
* @returns {Cleanup} - Cleanup function for the effect
|
|
23
|
-
*/
|
|
24
|
-
const createEffect = (callback: EffectCallback): Cleanup => {
|
|
25
|
-
if (!isFunction(callback) || callback.length > 1)
|
|
26
|
-
throw new InvalidCallbackError('effect', callback)
|
|
27
|
-
|
|
28
|
-
const isAsync = isAsyncFunction(callback)
|
|
29
|
-
let running = false
|
|
30
|
-
let controller: AbortController | undefined
|
|
31
|
-
|
|
32
|
-
const watcher = createWatcher(
|
|
33
|
-
() => {
|
|
34
|
-
watcher.run()
|
|
35
|
-
},
|
|
36
|
-
() => {
|
|
37
|
-
if (running) throw new CircularDependencyError('effect')
|
|
38
|
-
running = true
|
|
39
|
-
|
|
40
|
-
// Abort any previous async operations
|
|
41
|
-
controller?.abort()
|
|
42
|
-
controller = undefined
|
|
43
|
-
|
|
44
|
-
let cleanup: MaybeCleanup | Promise<MaybeCleanup>
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
if (isAsync) {
|
|
48
|
-
// Create AbortController for async callback
|
|
49
|
-
controller = new AbortController()
|
|
50
|
-
const currentController = controller
|
|
51
|
-
callback(controller.signal)
|
|
52
|
-
.then(cleanup => {
|
|
53
|
-
// Only register cleanup if this is still the current controller
|
|
54
|
-
if (
|
|
55
|
-
isFunction(cleanup) &&
|
|
56
|
-
controller === currentController
|
|
57
|
-
)
|
|
58
|
-
watcher.onCleanup(cleanup)
|
|
59
|
-
})
|
|
60
|
-
.catch(error => {
|
|
61
|
-
if (!isAbortError(error))
|
|
62
|
-
console.error(
|
|
63
|
-
'Error in async effect callback:',
|
|
64
|
-
error,
|
|
65
|
-
)
|
|
66
|
-
})
|
|
67
|
-
} else {
|
|
68
|
-
cleanup = callback()
|
|
69
|
-
if (isFunction(cleanup)) watcher.onCleanup(cleanup)
|
|
70
|
-
}
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (!isAbortError(error))
|
|
73
|
-
console.error('Error in effect callback:', error)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
running = false
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
watcher()
|
|
81
|
-
return () => {
|
|
82
|
-
controller?.abort()
|
|
83
|
-
try {
|
|
84
|
-
watcher.stop()
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error('Error in effect cleanup:', error)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* === Exports === */
|
|
92
|
-
|
|
93
|
-
export { type MaybeCleanup, type EffectCallback, createEffect }
|
package/src/match.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { createError } from './errors'
|
|
2
|
-
import type { ResolveResult } from './resolve'
|
|
3
|
-
import type { SignalValues, UnknownSignalRecord } from './signal'
|
|
4
|
-
|
|
5
|
-
/* === Types === */
|
|
6
|
-
|
|
7
|
-
type MatchHandlers<S extends UnknownSignalRecord> = {
|
|
8
|
-
ok: (values: SignalValues<S>) => void
|
|
9
|
-
err?: (errors: readonly Error[]) => void
|
|
10
|
-
nil?: () => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* === Functions === */
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Match on resolve result and call appropriate handler for side effects
|
|
17
|
-
*
|
|
18
|
-
* This is a utility function for those who prefer the handler pattern.
|
|
19
|
-
* All handlers are for side effects only and return void. If you need
|
|
20
|
-
* cleanup logic, use a hoisted let variable in your effect.
|
|
21
|
-
*
|
|
22
|
-
* @since 0.15.0
|
|
23
|
-
* @param {ResolveResult<S>} result - Result from resolve()
|
|
24
|
-
* @param {MatchHandlers<S>} handlers - Handlers for different states (side effects only)
|
|
25
|
-
* @returns {void} - Always returns void
|
|
26
|
-
*/
|
|
27
|
-
function match<S extends UnknownSignalRecord>(
|
|
28
|
-
result: ResolveResult<S>,
|
|
29
|
-
handlers: MatchHandlers<S>,
|
|
30
|
-
): void {
|
|
31
|
-
try {
|
|
32
|
-
if (result.pending) handlers.nil?.()
|
|
33
|
-
else if (result.errors) handlers.err?.(result.errors)
|
|
34
|
-
else if (result.ok) handlers.ok(result.values)
|
|
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])
|
|
39
|
-
else throw error
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* === Exports === */
|
|
44
|
-
|
|
45
|
-
export { match, type MatchHandlers }
|
package/src/resolve.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { UnknownRecord } from './diff'
|
|
2
|
-
import { createError } from './errors'
|
|
3
|
-
import type { SignalValues, UnknownSignalRecord } from './signal'
|
|
4
|
-
import { UNSET } from './system'
|
|
5
|
-
|
|
6
|
-
/* === Types === */
|
|
7
|
-
|
|
8
|
-
type ResolveResult<S extends UnknownSignalRecord> =
|
|
9
|
-
| { ok: true; values: SignalValues<S>; errors?: never; pending?: never }
|
|
10
|
-
| { ok: false; errors: readonly Error[]; values?: never; pending?: never }
|
|
11
|
-
| { ok: false; pending: true; values?: never; errors?: never }
|
|
12
|
-
|
|
13
|
-
/* === Functions === */
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Resolve signal values with perfect type inference
|
|
17
|
-
*
|
|
18
|
-
* Always returns a discriminated union result, regardless of whether
|
|
19
|
-
* handlers are provided or not. This ensures a predictable API.
|
|
20
|
-
*
|
|
21
|
-
* @since 0.15.0
|
|
22
|
-
* @param {S} signals - Signals to resolve
|
|
23
|
-
* @returns {ResolveResult<S>} - Discriminated union result
|
|
24
|
-
*/
|
|
25
|
-
function resolve<S extends UnknownSignalRecord>(signals: S): ResolveResult<S> {
|
|
26
|
-
const errors: Error[] = []
|
|
27
|
-
let pending = false
|
|
28
|
-
const values: UnknownRecord = {}
|
|
29
|
-
|
|
30
|
-
// Collect values and errors
|
|
31
|
-
for (const [key, signal] of Object.entries(signals)) {
|
|
32
|
-
try {
|
|
33
|
-
const value = signal.get()
|
|
34
|
-
if (value === UNSET) pending = true
|
|
35
|
-
else values[key] = value
|
|
36
|
-
} catch (e) {
|
|
37
|
-
errors.push(createError(e))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Return discriminated union
|
|
42
|
-
if (pending) return { ok: false, pending: true }
|
|
43
|
-
if (errors.length > 0) return { ok: false, errors }
|
|
44
|
-
return { ok: true, values: values as SignalValues<S> }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/* === Exports === */
|
|
48
|
-
|
|
49
|
-
export { resolve, type ResolveResult }
|