@zeix/cause-effect 0.17.2 → 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 +11 -5
- package/.github/copilot-instructions.md +1 -1
- package/.zed/settings.json +3 -0
- package/CLAUDE.md +18 -79
- package/README.md +23 -37
- package/archive/benchmark.ts +0 -5
- package/archive/collection.ts +5 -62
- package/archive/composite.ts +85 -0
- package/archive/computed.ts +17 -20
- package/archive/list.ts +6 -67
- package/archive/memo.ts +13 -14
- package/archive/store.ts +7 -66
- package/archive/task.ts +18 -20
- package/index.dev.js +438 -614
- package/index.js +1 -1
- package/index.ts +8 -19
- package/package.json +6 -6
- package/src/classes/collection.ts +59 -112
- package/src/classes/computed.ts +146 -189
- package/src/classes/list.ts +138 -105
- package/src/classes/ref.ts +16 -42
- package/src/classes/state.ts +16 -45
- package/src/classes/store.ts +107 -72
- package/src/effect.ts +9 -12
- package/src/errors.ts +12 -8
- package/src/signal.ts +3 -1
- package/src/system.ts +136 -154
- package/test/batch.test.ts +4 -11
- package/test/benchmark.test.ts +4 -2
- package/test/collection.test.ts +46 -306
- package/test/computed.test.ts +205 -223
- package/test/list.test.ts +35 -303
- package/test/ref.test.ts +38 -66
- package/test/state.test.ts +6 -12
- package/test/store.test.ts +37 -489
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +2 -2
- package/types/src/classes/collection.d.ts +4 -6
- package/types/src/classes/computed.d.ts +17 -37
- package/types/src/classes/list.d.ts +8 -6
- package/types/src/classes/ref.d.ts +7 -20
- package/types/src/classes/state.d.ts +5 -17
- package/types/src/classes/store.d.ts +12 -11
- package/types/src/errors.d.ts +2 -4
- package/types/src/signal.d.ts +3 -2
- package/types/src/system.d.ts +41 -44
- package/src/classes/composite.ts +0 -171
- package/types/src/classes/composite.d.ts +0 -15
package/src/classes/store.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { diff, type UnknownRecord } from '../diff'
|
|
1
|
+
import { type DiffResult, diff, type UnknownRecord } from '../diff'
|
|
2
2
|
import {
|
|
3
3
|
DuplicateKeyError,
|
|
4
|
-
|
|
4
|
+
guardMutableSignal,
|
|
5
5
|
validateSignalValue,
|
|
6
6
|
} from '../errors'
|
|
7
|
-
import { createMutableSignal, type MutableSignal
|
|
7
|
+
import { createMutableSignal, type MutableSignal } from '../signal'
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type Hook,
|
|
15
|
-
type HookCallback,
|
|
16
|
-
isHandledHook,
|
|
17
|
-
notifyWatchers,
|
|
18
|
-
subscribeActiveWatcher,
|
|
9
|
+
batch,
|
|
10
|
+
notifyOf,
|
|
11
|
+
registerWatchCallbacks,
|
|
12
|
+
type SignalOptions,
|
|
13
|
+
subscribeTo,
|
|
19
14
|
UNSET,
|
|
20
|
-
|
|
15
|
+
unsubscribeAllFrom,
|
|
21
16
|
} from '../system'
|
|
22
17
|
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
|
|
23
|
-
import { Composite } from './composite'
|
|
24
18
|
import type { List } from './list'
|
|
25
19
|
import type { State } from './state'
|
|
26
20
|
|
|
@@ -40,41 +34,93 @@ const TYPE_STORE = 'Store' as const
|
|
|
40
34
|
|
|
41
35
|
/* === Store Implementation === */
|
|
42
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
|
+
*/
|
|
43
45
|
class BaseStore<T extends UnknownRecord> {
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* Create a new store with the given initial value.
|
|
50
|
-
*
|
|
51
|
-
* @param {T} initialValue - The initial value of the store
|
|
52
|
-
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
53
|
-
* @throws {InvalidSignalValueError} - If the initial value is not an object
|
|
54
|
-
*/
|
|
55
|
-
constructor(initialValue: T) {
|
|
56
|
-
validateSignalValue(TYPE_STORE, initialValue, isRecord)
|
|
57
|
-
|
|
58
|
-
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,
|
|
59
51
|
initialValue,
|
|
60
|
-
|
|
61
|
-
key: K,
|
|
62
|
-
value: unknown,
|
|
63
|
-
): value is T[K] & {} => {
|
|
64
|
-
validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
|
|
65
|
-
return true
|
|
66
|
-
},
|
|
67
|
-
value => createMutableSignal(value),
|
|
52
|
+
options?.guard ?? isRecord,
|
|
68
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)
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
get #value(): T {
|
|
72
66
|
const record = {} as UnknownRecord
|
|
73
|
-
for (const [key, signal] of this.#
|
|
67
|
+
for (const [key, signal] of this.#signals.entries())
|
|
74
68
|
record[key] = signal.get()
|
|
75
69
|
return record as T
|
|
76
70
|
}
|
|
77
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
|
+
|
|
78
124
|
// Public methods
|
|
79
125
|
get [Symbol.toStringTag](): 'Store' {
|
|
80
126
|
return TYPE_STORE
|
|
@@ -87,12 +133,12 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
87
133
|
*[Symbol.iterator](): IterableIterator<
|
|
88
134
|
[string, MutableSignal<T[keyof T] & {}>]
|
|
89
135
|
> {
|
|
90
|
-
for (const [key, signal] of this.#
|
|
91
|
-
yield [key, signal as MutableSignal<T[keyof T] & {}>]
|
|
136
|
+
for (const [key, signal] of this.#signals.entries()) yield [key, signal]
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
keys(): IterableIterator<string> {
|
|
95
|
-
|
|
140
|
+
subscribeTo(this)
|
|
141
|
+
return this.#signals.keys()
|
|
96
142
|
}
|
|
97
143
|
|
|
98
144
|
byKey<K extends keyof T & string>(
|
|
@@ -104,9 +150,8 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
104
150
|
: T[K] extends unknown & {}
|
|
105
151
|
? State<T[K] & {}>
|
|
106
152
|
: State<T[K] & {}> | undefined {
|
|
107
|
-
return this.#
|
|
108
|
-
|
|
109
|
-
) as T[K] extends readonly (infer U extends {})[]
|
|
153
|
+
return this.#signals.get(key) as T[K] extends readonly (infer U extends
|
|
154
|
+
{})[]
|
|
110
155
|
? List<U>
|
|
111
156
|
: T[K] extends UnknownRecord
|
|
112
157
|
? Store<T[K]>
|
|
@@ -116,21 +161,20 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
116
161
|
}
|
|
117
162
|
|
|
118
163
|
get(): T {
|
|
119
|
-
|
|
164
|
+
subscribeTo(this)
|
|
120
165
|
return this.#value
|
|
121
166
|
}
|
|
122
167
|
|
|
123
168
|
set(newValue: T): void {
|
|
124
169
|
if (UNSET === newValue) {
|
|
125
|
-
this.#
|
|
126
|
-
|
|
127
|
-
this
|
|
170
|
+
this.#signals.clear()
|
|
171
|
+
notifyOf(this)
|
|
172
|
+
unsubscribeAllFrom(this)
|
|
128
173
|
return
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
if (changed) notifyWatchers(this.#watchers)
|
|
176
|
+
const changed = this.#change(diff(this.#value, newValue))
|
|
177
|
+
if (changed) notifyOf(this)
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
update(fn: (oldValue: T) => T): void {
|
|
@@ -138,30 +182,17 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
141
|
-
if (this.#
|
|
185
|
+
if (this.#signals.has(key))
|
|
142
186
|
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
143
187
|
|
|
144
|
-
const ok = this.#
|
|
145
|
-
if (ok)
|
|
188
|
+
const ok = this.#add(key, value)
|
|
189
|
+
if (ok) notifyOf(this)
|
|
146
190
|
return key
|
|
147
191
|
}
|
|
148
192
|
|
|
149
193
|
remove(key: string): void {
|
|
150
|
-
const ok = this.#
|
|
151
|
-
if (ok)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
on(type: Hook, callback: HookCallback): Cleanup {
|
|
155
|
-
if (type === HOOK_WATCH) {
|
|
156
|
-
this.#watchHookCallbacks ||= new Set<HookCallback>()
|
|
157
|
-
this.#watchHookCallbacks.add(callback)
|
|
158
|
-
return () => {
|
|
159
|
-
this.#watchHookCallbacks?.delete(callback)
|
|
160
|
-
}
|
|
161
|
-
} else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
|
|
162
|
-
return this.#composite.on(type, callback)
|
|
163
|
-
}
|
|
164
|
-
throw new InvalidHookError(TYPE_STORE, type)
|
|
194
|
+
const ok = this.#signals.delete(key)
|
|
195
|
+
if (ok) notifyOf(this)
|
|
165
196
|
}
|
|
166
197
|
}
|
|
167
198
|
|
|
@@ -172,10 +203,14 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
172
203
|
*
|
|
173
204
|
* @since 0.15.0
|
|
174
205
|
* @param {T} initialValue - Initial object or array value of the store
|
|
206
|
+
* @param {SignalOptions<T>} options - Options for the store
|
|
175
207
|
* @returns {Store<T>} - New store with reactive properties that preserves the original type T
|
|
176
208
|
*/
|
|
177
|
-
const createStore = <T extends UnknownRecord>(
|
|
178
|
-
|
|
209
|
+
const createStore = <T extends UnknownRecord>(
|
|
210
|
+
initialValue: T,
|
|
211
|
+
options?: SignalOptions<T>,
|
|
212
|
+
): Store<T> => {
|
|
213
|
+
const instance = new BaseStore(initialValue, options)
|
|
179
214
|
|
|
180
215
|
// Return proxy for property access
|
|
181
216
|
return new Proxy(instance, {
|
package/src/effect.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
-
import {
|
|
3
|
-
type Cleanup,
|
|
4
|
-
createWatcher,
|
|
5
|
-
HOOK_CLEANUP,
|
|
6
|
-
type MaybeCleanup,
|
|
7
|
-
trackSignalReads,
|
|
8
|
-
} from './system'
|
|
2
|
+
import { type Cleanup, createWatcher, type MaybeCleanup } from './system'
|
|
9
3
|
import { isAbortError, isAsyncFunction, isFunction } from './util'
|
|
10
4
|
|
|
11
5
|
/* === Types === */
|
|
@@ -35,8 +29,11 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
35
29
|
let running = false
|
|
36
30
|
let controller: AbortController | undefined
|
|
37
31
|
|
|
38
|
-
const watcher = createWatcher(
|
|
39
|
-
|
|
32
|
+
const watcher = createWatcher(
|
|
33
|
+
() => {
|
|
34
|
+
watcher.run()
|
|
35
|
+
},
|
|
36
|
+
() => {
|
|
40
37
|
if (running) throw new CircularDependencyError('effect')
|
|
41
38
|
running = true
|
|
42
39
|
|
|
@@ -58,7 +55,7 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
58
55
|
isFunction(cleanup) &&
|
|
59
56
|
controller === currentController
|
|
60
57
|
)
|
|
61
|
-
watcher.
|
|
58
|
+
watcher.onCleanup(cleanup)
|
|
62
59
|
})
|
|
63
60
|
.catch(error => {
|
|
64
61
|
if (!isAbortError(error))
|
|
@@ -69,7 +66,7 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
69
66
|
})
|
|
70
67
|
} else {
|
|
71
68
|
cleanup = callback()
|
|
72
|
-
if (isFunction(cleanup)) watcher.
|
|
69
|
+
if (isFunction(cleanup)) watcher.onCleanup(cleanup)
|
|
73
70
|
}
|
|
74
71
|
} catch (error) {
|
|
75
72
|
if (!isAbortError(error))
|
|
@@ -77,7 +74,7 @@ const createEffect = (callback: EffectCallback): Cleanup => {
|
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
running = false
|
|
80
|
-
}
|
|
77
|
+
},
|
|
81
78
|
)
|
|
82
79
|
|
|
83
80
|
watcher()
|
package/src/errors.ts
CHANGED
|
@@ -26,6 +26,13 @@ class DuplicateKeyError extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
class FailedAssertionError extends Error {
|
|
30
|
+
constructor(message: string = 'unexpected condition') {
|
|
31
|
+
super(`Assertion failed: ${message}`)
|
|
32
|
+
this.name = 'FailedAssertionError'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
class InvalidCallbackError extends TypeError {
|
|
30
37
|
constructor(where: string, value: unknown) {
|
|
31
38
|
super(`Invalid ${where} callback ${valueString(value)}`)
|
|
@@ -40,13 +47,6 @@ class InvalidCollectionSourceError extends TypeError {
|
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
class InvalidHookError extends TypeError {
|
|
44
|
-
constructor(where: string, type: string) {
|
|
45
|
-
super(`Invalid hook "${type}" in ${where}`)
|
|
46
|
-
this.name = 'InvalidHookError'
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
50
|
class InvalidSignalValueError extends TypeError {
|
|
51
51
|
constructor(where: string, value: unknown) {
|
|
52
52
|
super(`Invalid signal value ${valueString(value)} in ${where}`)
|
|
@@ -72,6 +72,10 @@ class ReadonlySignalError extends Error {
|
|
|
72
72
|
|
|
73
73
|
/* === Functions === */
|
|
74
74
|
|
|
75
|
+
function assert(condition: unknown, msg?: string): asserts condition {
|
|
76
|
+
if (!condition) throw new FailedAssertionError(msg)
|
|
77
|
+
}
|
|
78
|
+
|
|
75
79
|
const createError = /*#__PURE__*/ (reason: unknown): Error =>
|
|
76
80
|
reason instanceof Error ? reason : Error(String(reason))
|
|
77
81
|
|
|
@@ -108,10 +112,10 @@ export {
|
|
|
108
112
|
DuplicateKeyError,
|
|
109
113
|
InvalidCallbackError,
|
|
110
114
|
InvalidCollectionSourceError,
|
|
111
|
-
InvalidHookError,
|
|
112
115
|
InvalidSignalValueError,
|
|
113
116
|
NullishSignalValueError,
|
|
114
117
|
ReadonlySignalError,
|
|
118
|
+
assert,
|
|
115
119
|
createError,
|
|
116
120
|
validateCallback,
|
|
117
121
|
validateSignalValue,
|
package/src/signal.ts
CHANGED
|
@@ -18,6 +18,7 @@ type Signal<T extends {}> = {
|
|
|
18
18
|
get(): T
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
type UnknownSignal = Signal<unknown & {}>
|
|
21
22
|
type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
|
|
22
23
|
? List<U>
|
|
23
24
|
: T extends UnknownRecord
|
|
@@ -25,7 +26,7 @@ type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
|
|
|
25
26
|
: State<T>
|
|
26
27
|
type ReadonlySignal<T extends {}> = Computed<T> // | Collection<T>
|
|
27
28
|
|
|
28
|
-
type UnknownSignalRecord = Record<string,
|
|
29
|
+
type UnknownSignalRecord = Record<string, UnknownSignal>
|
|
29
30
|
|
|
30
31
|
type SignalValues<S extends UnknownSignalRecord> = {
|
|
31
32
|
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
@@ -100,5 +101,6 @@ export {
|
|
|
100
101
|
type ReadonlySignal,
|
|
101
102
|
type Signal,
|
|
102
103
|
type SignalValues,
|
|
104
|
+
type UnknownSignal,
|
|
103
105
|
type UnknownSignalRecord,
|
|
104
106
|
}
|