@zeix/cause-effect 0.17.2 → 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 -226
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +197 -202
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +241 -220
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1326 -1174
- package/index.js +1 -1
- package/index.ts +58 -85
- package/package.json +9 -6
- package/src/errors.ts +118 -70
- 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 -64
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -69
- package/test/benchmark.test.ts +473 -485
- package/test/collection.test.ts +455 -955
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +332 -857
- 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 -271
- package/test/store.test.ts +346 -898
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -19
- 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 -28
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -688
- package/archive/collection.ts +0 -310
- package/archive/computed.ts +0 -198
- package/archive/list.ts +0 -544
- package/archive/memo.ts +0 -140
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -357
- package/archive/task.ts +0 -191
- package/src/classes/collection.ts +0 -298
- package/src/classes/composite.ts +0 -171
- package/src/classes/computed.ts +0 -392
- package/src/classes/list.ts +0 -310
- package/src/classes/ref.ts +0 -96
- package/src/classes/state.ts +0 -131
- package/src/classes/store.ts +0 -227
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -96
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -275
- package/test/computed.test.ts +0 -1126
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -381
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -47
- package/types/src/classes/composite.d.ts +0 -15
- package/types/src/classes/computed.d.ts +0 -114
- package/types/src/classes/list.d.ts +0 -41
- package/types/src/classes/ref.d.ts +0 -48
- package/types/src/classes/state.d.ts +0 -61
- package/types/src/classes/store.d.ts +0 -51
- 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 -81
package/src/classes/ref.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { type Guard, InvalidHookError, validateSignalValue } from '../errors'
|
|
2
|
-
import {
|
|
3
|
-
type Cleanup,
|
|
4
|
-
HOOK_WATCH,
|
|
5
|
-
type HookCallback,
|
|
6
|
-
notifyWatchers,
|
|
7
|
-
subscribeActiveWatcher,
|
|
8
|
-
type Watcher,
|
|
9
|
-
type WatchHook,
|
|
10
|
-
} from '../system'
|
|
11
|
-
import { isObjectOfType } from '../util'
|
|
12
|
-
|
|
13
|
-
/* === Constants === */
|
|
14
|
-
|
|
15
|
-
const TYPE_REF = 'Ref'
|
|
16
|
-
|
|
17
|
-
/* === Class === */
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a new ref signal.
|
|
21
|
-
*
|
|
22
|
-
* @since 0.17.1
|
|
23
|
-
*/
|
|
24
|
-
class Ref<T extends {}> {
|
|
25
|
-
#watchers = new Set<Watcher>()
|
|
26
|
-
#value: T
|
|
27
|
-
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Create a new ref signal.
|
|
31
|
-
*
|
|
32
|
-
* @param {T} value - Reference to external object
|
|
33
|
-
* @param {Guard<T>} guard - Optional guard function to validate the value
|
|
34
|
-
* @throws {NullishSignalValueError} - If the value is null or undefined
|
|
35
|
-
* @throws {InvalidSignalValueError} - If the value is invalid
|
|
36
|
-
*/
|
|
37
|
-
constructor(value: T, guard?: Guard<T>) {
|
|
38
|
-
validateSignalValue(TYPE_REF, value, guard)
|
|
39
|
-
|
|
40
|
-
this.#value = value
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get [Symbol.toStringTag](): string {
|
|
44
|
-
return TYPE_REF
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get the value of the ref signal.
|
|
49
|
-
*
|
|
50
|
-
* @returns {T} - Object reference
|
|
51
|
-
*/
|
|
52
|
-
get(): T {
|
|
53
|
-
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
54
|
-
|
|
55
|
-
return this.#value
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Notify watchers of relevant changes in the external reference.
|
|
60
|
-
*/
|
|
61
|
-
notify(): void {
|
|
62
|
-
notifyWatchers(this.#watchers)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Register a callback to be called when HOOK_WATCH is triggered.
|
|
67
|
-
*
|
|
68
|
-
* @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
|
|
69
|
-
* @param {HookCallback} callback - The callback to register
|
|
70
|
-
* @returns {Cleanup} - A function to unregister the callback
|
|
71
|
-
*/
|
|
72
|
-
on(type: WatchHook, callback: HookCallback): Cleanup {
|
|
73
|
-
if (type === HOOK_WATCH) {
|
|
74
|
-
this.#watchHookCallbacks ||= new Set()
|
|
75
|
-
this.#watchHookCallbacks.add(callback)
|
|
76
|
-
return () => {
|
|
77
|
-
this.#watchHookCallbacks?.delete(callback)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
throw new InvalidHookError(TYPE_REF, type)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* === Functions === */
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if the provided value is a Ref instance
|
|
88
|
-
*
|
|
89
|
-
* @since 0.17.1
|
|
90
|
-
* @param {unknown} value - Value to check
|
|
91
|
-
* @returns {boolean} - True if the value is a Ref instance, false otherwise
|
|
92
|
-
*/
|
|
93
|
-
const isRef = /*#__PURE__*/ <T extends {}>(value: unknown): value is Ref<T> =>
|
|
94
|
-
isObjectOfType(value, TYPE_REF)
|
|
95
|
-
|
|
96
|
-
export { TYPE_REF, Ref, isRef }
|
package/src/classes/state.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { isEqual } from '../diff'
|
|
2
|
-
import {
|
|
3
|
-
InvalidHookError,
|
|
4
|
-
validateCallback,
|
|
5
|
-
validateSignalValue,
|
|
6
|
-
} from '../errors'
|
|
7
|
-
import {
|
|
8
|
-
type Cleanup,
|
|
9
|
-
HOOK_WATCH,
|
|
10
|
-
type HookCallback,
|
|
11
|
-
notifyWatchers,
|
|
12
|
-
subscribeActiveWatcher,
|
|
13
|
-
UNSET,
|
|
14
|
-
type Watcher,
|
|
15
|
-
type WatchHook,
|
|
16
|
-
} from '../system'
|
|
17
|
-
import { isObjectOfType } from '../util'
|
|
18
|
-
|
|
19
|
-
/* === Constants === */
|
|
20
|
-
|
|
21
|
-
const TYPE_STATE = 'State' as const
|
|
22
|
-
|
|
23
|
-
/* === Class === */
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Create a new state signal.
|
|
27
|
-
*
|
|
28
|
-
* @since 0.17.0
|
|
29
|
-
*/
|
|
30
|
-
class State<T extends {}> {
|
|
31
|
-
#watchers = new Set<Watcher>()
|
|
32
|
-
#value: T
|
|
33
|
-
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create a new state signal.
|
|
37
|
-
*
|
|
38
|
-
* @param {T} initialValue - Initial value of the state
|
|
39
|
-
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
40
|
-
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
41
|
-
*/
|
|
42
|
-
constructor(initialValue: T) {
|
|
43
|
-
validateSignalValue(TYPE_STATE, initialValue)
|
|
44
|
-
|
|
45
|
-
this.#value = initialValue
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
get [Symbol.toStringTag](): string {
|
|
49
|
-
return TYPE_STATE
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the current value of the state signal.
|
|
54
|
-
*
|
|
55
|
-
* @returns {T} - Current value of the state
|
|
56
|
-
*/
|
|
57
|
-
get(): T {
|
|
58
|
-
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
59
|
-
|
|
60
|
-
return this.#value
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Set the value of the state signal.
|
|
65
|
-
*
|
|
66
|
-
* @param {T} newValue - New value of the state
|
|
67
|
-
* @returns {void}
|
|
68
|
-
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
69
|
-
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
70
|
-
*/
|
|
71
|
-
set(newValue: T): void {
|
|
72
|
-
validateSignalValue(TYPE_STATE, newValue)
|
|
73
|
-
|
|
74
|
-
if (isEqual(this.#value, newValue)) return
|
|
75
|
-
this.#value = newValue
|
|
76
|
-
if (this.#watchers.size) notifyWatchers(this.#watchers)
|
|
77
|
-
|
|
78
|
-
// Setting to UNSET clears the watchers so the signal can be garbage collected
|
|
79
|
-
if (UNSET === this.#value) this.#watchers.clear()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Update the value of the state signal.
|
|
84
|
-
*
|
|
85
|
-
* @param {Function} updater - Function that takes the current value and returns the new value
|
|
86
|
-
* @returns {void}
|
|
87
|
-
* @throws {InvalidCallbackError} - If the updater function is not a function
|
|
88
|
-
* @throws {NullishSignalValueError} - If the initial value is null or undefined
|
|
89
|
-
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
90
|
-
*/
|
|
91
|
-
update(updater: (oldValue: T) => T): void {
|
|
92
|
-
validateCallback(`${TYPE_STATE} update`, updater)
|
|
93
|
-
|
|
94
|
-
this.set(updater(this.#value))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Register a callback to be called when HOOK_WATCH is triggered.
|
|
99
|
-
*
|
|
100
|
-
* @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
|
|
101
|
-
* @param {HookCallback} callback - The callback to register
|
|
102
|
-
* @returns {Cleanup} - A function to unregister the callback
|
|
103
|
-
*/
|
|
104
|
-
on(type: WatchHook, callback: HookCallback): Cleanup {
|
|
105
|
-
if (type === HOOK_WATCH) {
|
|
106
|
-
this.#watchHookCallbacks ||= new Set()
|
|
107
|
-
this.#watchHookCallbacks.add(callback)
|
|
108
|
-
return () => {
|
|
109
|
-
this.#watchHookCallbacks?.delete(callback)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
throw new InvalidHookError(this.constructor.name, type)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* === Functions === */
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check if the provided value is a State instance
|
|
120
|
-
*
|
|
121
|
-
* @since 0.9.0
|
|
122
|
-
* @param {unknown} value - Value to check
|
|
123
|
-
* @returns {boolean} - True if the value is a State instance, false otherwise
|
|
124
|
-
*/
|
|
125
|
-
const isState = /*#__PURE__*/ <T extends {}>(
|
|
126
|
-
value: unknown,
|
|
127
|
-
): value is State<T> => isObjectOfType(value, TYPE_STATE)
|
|
128
|
-
|
|
129
|
-
/* === Exports === */
|
|
130
|
-
|
|
131
|
-
export { TYPE_STATE, isState, State }
|
package/src/classes/store.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { diff, type UnknownRecord } from '../diff'
|
|
2
|
-
import {
|
|
3
|
-
DuplicateKeyError,
|
|
4
|
-
InvalidHookError,
|
|
5
|
-
validateSignalValue,
|
|
6
|
-
} from '../errors'
|
|
7
|
-
import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
|
|
8
|
-
import {
|
|
9
|
-
type Cleanup,
|
|
10
|
-
HOOK_ADD,
|
|
11
|
-
HOOK_CHANGE,
|
|
12
|
-
HOOK_REMOVE,
|
|
13
|
-
HOOK_WATCH,
|
|
14
|
-
type Hook,
|
|
15
|
-
type HookCallback,
|
|
16
|
-
isHandledHook,
|
|
17
|
-
notifyWatchers,
|
|
18
|
-
subscribeActiveWatcher,
|
|
19
|
-
UNSET,
|
|
20
|
-
type Watcher,
|
|
21
|
-
} from '../system'
|
|
22
|
-
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
|
|
23
|
-
import { Composite } from './composite'
|
|
24
|
-
import type { List } from './list'
|
|
25
|
-
import type { State } from './state'
|
|
26
|
-
|
|
27
|
-
/* === Types === */
|
|
28
|
-
|
|
29
|
-
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
30
|
-
[K in keyof T]: T[K] extends readonly (infer U extends {})[]
|
|
31
|
-
? List<U>
|
|
32
|
-
: T[K] extends UnknownRecord
|
|
33
|
-
? Store<T[K]>
|
|
34
|
-
: State<T[K] & {}>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* === Constants === */
|
|
38
|
-
|
|
39
|
-
const TYPE_STORE = 'Store' as const
|
|
40
|
-
|
|
41
|
-
/* === Store Implementation === */
|
|
42
|
-
|
|
43
|
-
class BaseStore<T extends UnknownRecord> {
|
|
44
|
-
#composite: Composite<T, Signal<T[keyof T] & {}>>
|
|
45
|
-
#watchers = new Set<Watcher>()
|
|
46
|
-
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
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] & {}>>(
|
|
59
|
-
initialValue,
|
|
60
|
-
<K extends keyof T & string>(
|
|
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),
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
get #value(): T {
|
|
72
|
-
const record = {} as UnknownRecord
|
|
73
|
-
for (const [key, signal] of this.#composite.signals.entries())
|
|
74
|
-
record[key] = signal.get()
|
|
75
|
-
return record as T
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Public methods
|
|
79
|
-
get [Symbol.toStringTag](): 'Store' {
|
|
80
|
-
return TYPE_STORE
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
get [Symbol.isConcatSpreadable](): boolean {
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
*[Symbol.iterator](): IterableIterator<
|
|
88
|
-
[string, MutableSignal<T[keyof T] & {}>]
|
|
89
|
-
> {
|
|
90
|
-
for (const [key, signal] of this.#composite.signals.entries())
|
|
91
|
-
yield [key, signal as MutableSignal<T[keyof T] & {}>]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
keys(): IterableIterator<string> {
|
|
95
|
-
return this.#composite.signals.keys()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
byKey<K extends keyof T & string>(
|
|
99
|
-
key: K,
|
|
100
|
-
): T[K] extends readonly (infer U extends {})[]
|
|
101
|
-
? List<U>
|
|
102
|
-
: T[K] extends UnknownRecord
|
|
103
|
-
? Store<T[K]>
|
|
104
|
-
: T[K] extends unknown & {}
|
|
105
|
-
? State<T[K] & {}>
|
|
106
|
-
: State<T[K] & {}> | undefined {
|
|
107
|
-
return this.#composite.signals.get(
|
|
108
|
-
key,
|
|
109
|
-
) as T[K] extends readonly (infer U extends {})[]
|
|
110
|
-
? List<U>
|
|
111
|
-
: T[K] extends UnknownRecord
|
|
112
|
-
? Store<T[K]>
|
|
113
|
-
: T[K] extends unknown & {}
|
|
114
|
-
? State<T[K] & {}>
|
|
115
|
-
: State<T[K] & {}> | undefined
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
get(): T {
|
|
119
|
-
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
120
|
-
return this.#value
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
set(newValue: T): void {
|
|
124
|
-
if (UNSET === newValue) {
|
|
125
|
-
this.#composite.clear()
|
|
126
|
-
notifyWatchers(this.#watchers)
|
|
127
|
-
this.#watchers.clear()
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const oldValue = this.#value
|
|
132
|
-
const changed = this.#composite.change(diff(oldValue, newValue))
|
|
133
|
-
if (changed) notifyWatchers(this.#watchers)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
update(fn: (oldValue: T) => T): void {
|
|
137
|
-
this.set(fn(this.get()))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
141
|
-
if (this.#composite.signals.has(key))
|
|
142
|
-
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
143
|
-
|
|
144
|
-
const ok = this.#composite.add(key, value)
|
|
145
|
-
if (ok) notifyWatchers(this.#watchers)
|
|
146
|
-
return key
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
remove(key: string): void {
|
|
150
|
-
const ok = this.#composite.remove(key)
|
|
151
|
-
if (ok) notifyWatchers(this.#watchers)
|
|
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)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* === Functions === */
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Create a new store with deeply nested reactive properties
|
|
172
|
-
*
|
|
173
|
-
* @since 0.15.0
|
|
174
|
-
* @param {T} initialValue - Initial object or array value of the store
|
|
175
|
-
* @returns {Store<T>} - New store with reactive properties that preserves the original type T
|
|
176
|
-
*/
|
|
177
|
-
const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
178
|
-
const instance = new BaseStore(initialValue)
|
|
179
|
-
|
|
180
|
-
// Return proxy for property access
|
|
181
|
-
return new Proxy(instance, {
|
|
182
|
-
get(target, prop) {
|
|
183
|
-
if (prop in target) {
|
|
184
|
-
const value = Reflect.get(target, prop)
|
|
185
|
-
return isFunction(value) ? value.bind(target) : value
|
|
186
|
-
}
|
|
187
|
-
if (!isSymbol(prop)) return target.byKey(prop)
|
|
188
|
-
},
|
|
189
|
-
has(target, prop) {
|
|
190
|
-
if (prop in target) return true
|
|
191
|
-
return target.byKey(String(prop)) !== undefined
|
|
192
|
-
},
|
|
193
|
-
ownKeys(target) {
|
|
194
|
-
return Array.from(target.keys())
|
|
195
|
-
},
|
|
196
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
197
|
-
if (prop in target)
|
|
198
|
-
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
199
|
-
if (isSymbol(prop)) return undefined
|
|
200
|
-
|
|
201
|
-
const signal = target.byKey(String(prop))
|
|
202
|
-
return signal
|
|
203
|
-
? {
|
|
204
|
-
enumerable: true,
|
|
205
|
-
configurable: true,
|
|
206
|
-
writable: true,
|
|
207
|
-
value: signal,
|
|
208
|
-
}
|
|
209
|
-
: undefined
|
|
210
|
-
},
|
|
211
|
-
}) as Store<T>
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Check if the provided value is a Store instance
|
|
216
|
-
*
|
|
217
|
-
* @since 0.15.0
|
|
218
|
-
* @param {unknown} value - Value to check
|
|
219
|
-
* @returns {boolean} - True if the value is a Store instance, false otherwise
|
|
220
|
-
*/
|
|
221
|
-
const isStore = <T extends UnknownRecord>(
|
|
222
|
-
value: unknown,
|
|
223
|
-
): value is BaseStore<T> => isObjectOfType(value, TYPE_STORE)
|
|
224
|
-
|
|
225
|
-
/* === Exports === */
|
|
226
|
-
|
|
227
|
-
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,96 +0,0 @@
|
|
|
1
|
-
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
-
import {
|
|
3
|
-
type Cleanup,
|
|
4
|
-
createWatcher,
|
|
5
|
-
HOOK_CLEANUP,
|
|
6
|
-
type MaybeCleanup,
|
|
7
|
-
trackSignalReads,
|
|
8
|
-
} from './system'
|
|
9
|
-
import { isAbortError, isAsyncFunction, isFunction } from './util'
|
|
10
|
-
|
|
11
|
-
/* === Types === */
|
|
12
|
-
|
|
13
|
-
type EffectCallback =
|
|
14
|
-
| (() => MaybeCleanup)
|
|
15
|
-
| ((abort: AbortSignal) => Promise<MaybeCleanup>)
|
|
16
|
-
|
|
17
|
-
/* === Functions === */
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Define what happens when a reactive state changes
|
|
21
|
-
*
|
|
22
|
-
* The callback can be synchronous or asynchronous. Async callbacks receive
|
|
23
|
-
* an AbortSignal parameter, which is automatically aborted when the effect
|
|
24
|
-
* re-runs or is cleaned up, preventing stale async operations.
|
|
25
|
-
*
|
|
26
|
-
* @since 0.1.0
|
|
27
|
-
* @param {EffectCallback} callback - Synchronous or asynchronous effect callback
|
|
28
|
-
* @returns {Cleanup} - Cleanup function for the effect
|
|
29
|
-
*/
|
|
30
|
-
const createEffect = (callback: EffectCallback): Cleanup => {
|
|
31
|
-
if (!isFunction(callback) || callback.length > 1)
|
|
32
|
-
throw new InvalidCallbackError('effect', callback)
|
|
33
|
-
|
|
34
|
-
const isAsync = isAsyncFunction(callback)
|
|
35
|
-
let running = false
|
|
36
|
-
let controller: AbortController | undefined
|
|
37
|
-
|
|
38
|
-
const watcher = createWatcher(() =>
|
|
39
|
-
trackSignalReads(watcher, () => {
|
|
40
|
-
if (running) throw new CircularDependencyError('effect')
|
|
41
|
-
running = true
|
|
42
|
-
|
|
43
|
-
// Abort any previous async operations
|
|
44
|
-
controller?.abort()
|
|
45
|
-
controller = undefined
|
|
46
|
-
|
|
47
|
-
let cleanup: MaybeCleanup | Promise<MaybeCleanup>
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
if (isAsync) {
|
|
51
|
-
// Create AbortController for async callback
|
|
52
|
-
controller = new AbortController()
|
|
53
|
-
const currentController = controller
|
|
54
|
-
callback(controller.signal)
|
|
55
|
-
.then(cleanup => {
|
|
56
|
-
// Only register cleanup if this is still the current controller
|
|
57
|
-
if (
|
|
58
|
-
isFunction(cleanup) &&
|
|
59
|
-
controller === currentController
|
|
60
|
-
)
|
|
61
|
-
watcher.on(HOOK_CLEANUP, cleanup)
|
|
62
|
-
})
|
|
63
|
-
.catch(error => {
|
|
64
|
-
if (!isAbortError(error))
|
|
65
|
-
console.error(
|
|
66
|
-
'Error in async effect callback:',
|
|
67
|
-
error,
|
|
68
|
-
)
|
|
69
|
-
})
|
|
70
|
-
} else {
|
|
71
|
-
cleanup = callback()
|
|
72
|
-
if (isFunction(cleanup)) watcher.on(HOOK_CLEANUP, cleanup)
|
|
73
|
-
}
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (!isAbortError(error))
|
|
76
|
-
console.error('Error in effect callback:', error)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
running = false
|
|
80
|
-
}),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
watcher()
|
|
84
|
-
return () => {
|
|
85
|
-
controller?.abort()
|
|
86
|
-
try {
|
|
87
|
-
watcher.stop()
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error('Error in effect cleanup:', error)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/* === Exports === */
|
|
95
|
-
|
|
96
|
-
export { type MaybeCleanup, type EffectCallback, createEffect }
|