@zeix/cause-effect 0.17.1 → 0.17.3
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 +13 -0
- package/.github/copilot-instructions.md +4 -0
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +41 -7
- package/README.md +48 -25
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +6 -65
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +18 -20
- package/archive/list.ts +7 -75
- package/archive/memo.ts +15 -15
- package/archive/state.ts +2 -1
- package/archive/store.ts +8 -78
- package/archive/task.ts +20 -25
- package/index.dev.js +508 -526
- package/index.js +1 -1
- package/index.ts +9 -11
- package/package.json +6 -6
- package/src/classes/collection.ts +70 -107
- package/src/classes/computed.ts +165 -149
- package/src/classes/list.ts +145 -107
- package/src/classes/ref.ts +19 -17
- package/src/classes/state.ts +21 -17
- package/src/classes/store.ts +125 -73
- package/src/diff.ts +2 -1
- package/src/effect.ts +17 -10
- package/src/errors.ts +14 -1
- package/src/resolve.ts +1 -1
- package/src/signal.ts +3 -2
- package/src/system.ts +159 -61
- package/src/util.ts +0 -6
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +106 -107
- package/test/computed.test.ts +351 -112
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +62 -102
- package/test/ref.test.ts +128 -2
- package/test/state.test.ts +16 -22
- package/test/store.test.ts +101 -108
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +3 -3
- package/types/src/classes/collection.d.ts +9 -10
- package/types/src/classes/computed.d.ts +17 -20
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +8 -12
- package/types/src/classes/state.d.ts +5 -8
- package/types/src/classes/store.d.ts +14 -13
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +2 -1
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +47 -34
- package/types/src/util.d.ts +1 -2
- package/src/classes/composite.ts +0 -176
- package/types/src/classes/composite.d.ts +0 -15
package/src/classes/store.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { diff, type UnknownRecord } from '../diff'
|
|
2
|
-
import { DuplicateKeyError, validateSignalValue } from '../errors'
|
|
3
|
-
import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
|
|
1
|
+
import { type DiffResult, diff, type UnknownRecord } from '../diff'
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
11
16
|
} from '../system'
|
|
12
|
-
import { isFunction, isObjectOfType, isRecord, isSymbol
|
|
13
|
-
import { Composite } from './composite'
|
|
17
|
+
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
|
|
14
18
|
import type { List } from './list'
|
|
15
19
|
import type { State } from './state'
|
|
16
20
|
|
|
@@ -30,40 +34,93 @@ const TYPE_STORE = 'Store' as const
|
|
|
30
34
|
|
|
31
35
|
/* === Store Implementation === */
|
|
32
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
|
+
*/
|
|
33
45
|
class BaseStore<T extends UnknownRecord> {
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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] & {}>>(
|
|
46
|
+
#signals = new Map<keyof T & string, MutableSignal<T[keyof T] & {}>>()
|
|
47
|
+
|
|
48
|
+
constructor(initialValue: T, options?: SignalOptions<T>) {
|
|
49
|
+
validateSignalValue(
|
|
50
|
+
TYPE_STORE,
|
|
48
51
|
initialValue,
|
|
49
|
-
|
|
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),
|
|
52
|
+
options?.guard ?? isRecord,
|
|
57
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)
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
get #value(): T {
|
|
61
66
|
const record = {} as UnknownRecord
|
|
62
|
-
for (const [key, signal] of this.#
|
|
67
|
+
for (const [key, signal] of this.#signals.entries())
|
|
63
68
|
record[key] = signal.get()
|
|
64
69
|
return record as T
|
|
65
70
|
}
|
|
66
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
|
+
|
|
67
124
|
// Public methods
|
|
68
125
|
get [Symbol.toStringTag](): 'Store' {
|
|
69
126
|
return TYPE_STORE
|
|
@@ -76,30 +133,12 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
76
133
|
*[Symbol.iterator](): IterableIterator<
|
|
77
134
|
[string, MutableSignal<T[keyof T] & {}>]
|
|
78
135
|
> {
|
|
79
|
-
for (const [key, signal] of this.#
|
|
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)
|
|
136
|
+
for (const [key, signal] of this.#signals.entries()) yield [key, signal]
|
|
99
137
|
}
|
|
100
138
|
|
|
101
139
|
keys(): IterableIterator<string> {
|
|
102
|
-
|
|
140
|
+
subscribeTo(this)
|
|
141
|
+
return this.#signals.keys()
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
byKey<K extends keyof T & string>(
|
|
@@ -111,9 +150,8 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
111
150
|
: T[K] extends unknown & {}
|
|
112
151
|
? State<T[K] & {}>
|
|
113
152
|
: State<T[K] & {}> | undefined {
|
|
114
|
-
return this.#
|
|
115
|
-
|
|
116
|
-
) as T[K] extends readonly (infer U extends {})[]
|
|
153
|
+
return this.#signals.get(key) as T[K] extends readonly (infer U extends
|
|
154
|
+
{})[]
|
|
117
155
|
? List<U>
|
|
118
156
|
: T[K] extends UnknownRecord
|
|
119
157
|
? Store<T[K]>
|
|
@@ -122,29 +160,39 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
122
160
|
: State<T[K] & {}> | undefined
|
|
123
161
|
}
|
|
124
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
|
+
|
|
125
180
|
update(fn: (oldValue: T) => T): void {
|
|
126
181
|
this.set(fn(this.get()))
|
|
127
182
|
}
|
|
128
183
|
|
|
129
184
|
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
130
|
-
if (this.#
|
|
131
|
-
throw new DuplicateKeyError(
|
|
185
|
+
if (this.#signals.has(key))
|
|
186
|
+
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
132
187
|
|
|
133
|
-
const ok = this.#
|
|
134
|
-
if (ok)
|
|
188
|
+
const ok = this.#add(key, value)
|
|
189
|
+
if (ok) notifyOf(this)
|
|
135
190
|
return key
|
|
136
191
|
}
|
|
137
192
|
|
|
138
193
|
remove(key: string): void {
|
|
139
|
-
const ok = this.#
|
|
140
|
-
if (ok)
|
|
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)
|
|
194
|
+
const ok = this.#signals.delete(key)
|
|
195
|
+
if (ok) notifyOf(this)
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
|
|
@@ -155,10 +203,14 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
155
203
|
*
|
|
156
204
|
* @since 0.15.0
|
|
157
205
|
* @param {T} initialValue - Initial object or array value of the store
|
|
206
|
+
* @param {SignalOptions<T>} options - Options for the store
|
|
158
207
|
* @returns {Store<T>} - New store with reactive properties that preserves the original type T
|
|
159
208
|
*/
|
|
160
|
-
const createStore = <T extends UnknownRecord>(
|
|
161
|
-
|
|
209
|
+
const createStore = <T extends UnknownRecord>(
|
|
210
|
+
initialValue: T,
|
|
211
|
+
options?: SignalOptions<T>,
|
|
212
|
+
): Store<T> => {
|
|
213
|
+
const instance = new BaseStore(initialValue, options)
|
|
162
214
|
|
|
163
215
|
// Return proxy for property access
|
|
164
216
|
return new Proxy(instance, {
|
package/src/diff.ts
CHANGED
package/src/effect.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
-
import { type Cleanup, createWatcher,
|
|
2
|
+
import { type Cleanup, createWatcher, type MaybeCleanup } from './system'
|
|
3
3
|
import { isAbortError, isAsyncFunction, isFunction } from './util'
|
|
4
4
|
|
|
5
5
|
/* === Types === */
|
|
6
6
|
|
|
7
|
-
// biome-ignore lint/suspicious/noConfusingVoidType: optional Cleanup return type
|
|
8
|
-
type MaybeCleanup = Cleanup | undefined | void
|
|
9
|
-
|
|
10
7
|
type EffectCallback =
|
|
11
8
|
| (() => MaybeCleanup)
|
|
12
9
|
| ((abort: AbortSignal) => Promise<MaybeCleanup>)
|
|
@@ -32,8 +29,11 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
32
29
|
let running = false
|
|
33
30
|
let controller: AbortController | undefined
|
|
34
31
|
|
|
35
|
-
const watcher = createWatcher(
|
|
36
|
-
|
|
32
|
+
const watcher = createWatcher(
|
|
33
|
+
() => {
|
|
34
|
+
watcher.run()
|
|
35
|
+
},
|
|
36
|
+
() => {
|
|
37
37
|
if (running) throw new CircularDependencyError('effect')
|
|
38
38
|
running = true
|
|
39
39
|
|
|
@@ -59,7 +59,10 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
59
59
|
})
|
|
60
60
|
.catch(error => {
|
|
61
61
|
if (!isAbortError(error))
|
|
62
|
-
console.error(
|
|
62
|
+
console.error(
|
|
63
|
+
'Error in async effect callback:',
|
|
64
|
+
error,
|
|
65
|
+
)
|
|
63
66
|
})
|
|
64
67
|
} else {
|
|
65
68
|
cleanup = callback()
|
|
@@ -67,17 +70,21 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
67
70
|
}
|
|
68
71
|
} catch (error) {
|
|
69
72
|
if (!isAbortError(error))
|
|
70
|
-
console.error('
|
|
73
|
+
console.error('Error in effect callback:', error)
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
running = false
|
|
74
|
-
}
|
|
77
|
+
},
|
|
75
78
|
)
|
|
76
79
|
|
|
77
80
|
watcher()
|
|
78
81
|
return () => {
|
|
79
82
|
controller?.abort()
|
|
80
|
-
|
|
83
|
+
try {
|
|
84
|
+
watcher.stop()
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error in effect cleanup:', error)
|
|
87
|
+
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isMutableSignal, type MutableSignal } from './signal'
|
|
2
|
-
import {
|
|
2
|
+
import { UNSET } from './system'
|
|
3
|
+
import { isFunction, isSymbol, valueString } from './util'
|
|
3
4
|
|
|
4
5
|
/* === Types === */
|
|
5
6
|
|
|
@@ -25,6 +26,13 @@ class DuplicateKeyError extends Error {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
class FailedAssertionError extends Error {
|
|
30
|
+
constructor(message: string = 'unexpected condition') {
|
|
31
|
+
super(`Assertion failed: ${message}`)
|
|
32
|
+
this.name = 'FailedAssertionError'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
class InvalidCallbackError extends TypeError {
|
|
29
37
|
constructor(where: string, value: unknown) {
|
|
30
38
|
super(`Invalid ${where} callback ${valueString(value)}`)
|
|
@@ -64,6 +72,10 @@ class ReadonlySignalError extends Error {
|
|
|
64
72
|
|
|
65
73
|
/* === Functions === */
|
|
66
74
|
|
|
75
|
+
function assert(condition: unknown, msg?: string): asserts condition {
|
|
76
|
+
if (!condition) throw new FailedAssertionError(msg)
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
const createError = /*#__PURE__*/ (reason: unknown): Error =>
|
|
68
80
|
reason instanceof Error ? reason : Error(String(reason))
|
|
69
81
|
|
|
@@ -103,6 +115,7 @@ export {
|
|
|
103
115
|
InvalidSignalValueError,
|
|
104
116
|
NullishSignalValueError,
|
|
105
117
|
ReadonlySignalError,
|
|
118
|
+
assert,
|
|
106
119
|
createError,
|
|
107
120
|
validateCallback,
|
|
108
121
|
validateSignalValue,
|
package/src/resolve.ts
CHANGED
package/src/signal.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { isList, List } from './classes/list'
|
|
|
10
10
|
import { isState, State } from './classes/state'
|
|
11
11
|
import { createStore, isStore, type Store } from './classes/store'
|
|
12
12
|
import type { UnknownRecord } from './diff'
|
|
13
|
-
// import type { Collection } from './signals/collection'
|
|
14
13
|
import { isRecord, isUniformArray } from './util'
|
|
15
14
|
|
|
16
15
|
/* === Types === */
|
|
@@ -19,6 +18,7 @@ type Signal<T extends {}> = {
|
|
|
19
18
|
get(): T
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
type UnknownSignal = Signal<unknown & {}>
|
|
22
22
|
type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
|
|
23
23
|
? List<U>
|
|
24
24
|
: T extends UnknownRecord
|
|
@@ -26,7 +26,7 @@ type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
|
|
|
26
26
|
: State<T>
|
|
27
27
|
type ReadonlySignal<T extends {}> = Computed<T> // | Collection<T>
|
|
28
28
|
|
|
29
|
-
type UnknownSignalRecord = Record<string,
|
|
29
|
+
type UnknownSignalRecord = Record<string, UnknownSignal>
|
|
30
30
|
|
|
31
31
|
type SignalValues<S extends UnknownSignalRecord> = {
|
|
32
32
|
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
@@ -101,5 +101,6 @@ export {
|
|
|
101
101
|
type ReadonlySignal,
|
|
102
102
|
type Signal,
|
|
103
103
|
type SignalValues,
|
|
104
|
+
type UnknownSignal,
|
|
104
105
|
type UnknownSignalRecord,
|
|
105
106
|
}
|