@zeix/cause-effect 0.14.1 → 0.15.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/README.md +256 -27
- package/biome.json +35 -0
- package/index.d.ts +32 -7
- package/index.dev.js +629 -0
- package/index.js +1 -1
- package/index.ts +41 -21
- package/package.json +6 -7
- package/src/computed.ts +30 -21
- package/src/diff.ts +136 -0
- package/src/effect.ts +59 -49
- package/src/match.ts +57 -0
- package/src/resolve.ts +58 -0
- package/src/scheduler.ts +3 -3
- package/src/signal.ts +48 -15
- package/src/state.ts +4 -3
- package/src/store.ts +325 -0
- package/src/util.ts +57 -5
- package/test/batch.test.ts +29 -25
- package/test/benchmark.test.ts +81 -45
- package/test/computed.test.ts +43 -39
- package/test/diff.test.ts +638 -0
- package/test/effect.test.ts +657 -49
- package/test/match.test.ts +378 -0
- package/test/resolve.test.ts +156 -0
- package/test/state.test.ts +33 -33
- package/test/store.test.ts +719 -0
- package/test/util/framework-types.ts +2 -2
- package/test/util/perf-tests.ts +2 -2
- package/test/util/reactive-framework.ts +1 -1
- package/tsconfig.json +9 -10
- package/types/index.d.ts +15 -0
- package/{src → types/src}/computed.d.ts +2 -2
- package/types/src/diff.d.ts +27 -0
- package/types/src/effect.d.ts +16 -0
- package/types/src/match.d.ts +21 -0
- package/types/src/resolve.d.ts +29 -0
- package/{src → types/src}/scheduler.d.ts +2 -2
- package/types/src/signal.d.ts +40 -0
- package/{src → types/src}/state.d.ts +1 -1
- package/types/src/store.d.ts +57 -0
- package/types/src/util.d.ts +15 -0
- package/types/test-new-effect.d.ts +1 -0
- package/src/effect.d.ts +0 -17
- package/src/signal.d.ts +0 -26
- package/src/util.d.ts +0 -7
package/src/signal.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { isState, state } from './state'
|
|
2
1
|
import {
|
|
2
|
+
type Computed,
|
|
3
3
|
type ComputedCallback,
|
|
4
|
+
computed,
|
|
4
5
|
isComputed,
|
|
5
6
|
isComputedCallback,
|
|
6
|
-
computed,
|
|
7
7
|
} from './computed'
|
|
8
|
+
import { isState, type State, state } from './state'
|
|
9
|
+
import { isStore, type Store, store } from './store'
|
|
10
|
+
import { arrayToRecord, isRecord } from './util'
|
|
8
11
|
|
|
9
12
|
/* === Types === */
|
|
10
13
|
|
|
@@ -13,12 +16,13 @@ type Signal<T extends {}> = {
|
|
|
13
16
|
}
|
|
14
17
|
type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
|
|
15
18
|
|
|
16
|
-
type SignalValues<S extends Signal<{}
|
|
19
|
+
type SignalValues<S extends Record<string, Signal<unknown & {}>>> = {
|
|
17
20
|
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/* === Constants === */
|
|
21
24
|
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: Deliberately using any to be used as a placeholder value in any signal
|
|
22
26
|
const UNSET: any = Symbol()
|
|
23
27
|
|
|
24
28
|
/* === Functions === */
|
|
@@ -32,23 +36,52 @@ const UNSET: any = Symbol()
|
|
|
32
36
|
*/
|
|
33
37
|
const isSignal = /*#__PURE__*/ <T extends {}>(
|
|
34
38
|
value: unknown,
|
|
35
|
-
): value is Signal<T> => isState(value) || isComputed(value)
|
|
39
|
+
): value is Signal<T> => isState(value) || isComputed(value) || isStore(value)
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Convert a value to a Signal if it's not already a Signal
|
|
39
43
|
*
|
|
40
44
|
* @since 0.9.6
|
|
41
|
-
* @param {MaybeSignal<T>} value - value to convert to a Signal
|
|
42
|
-
* @returns {Signal<T>} - converted Signal
|
|
43
45
|
*/
|
|
44
|
-
|
|
45
|
-
value:
|
|
46
|
-
):
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
function toSignal<T extends Array<unknown & {}>>(
|
|
47
|
+
value: T[],
|
|
48
|
+
): Store<Record<string, T>>
|
|
49
|
+
function toSignal<T extends Record<keyof T, T[keyof T]>>(value: T): Store<T>
|
|
50
|
+
function toSignal<T extends {}>(value: ComputedCallback<T>): Computed<T>
|
|
51
|
+
function toSignal<T extends {}>(value: Signal<T>): Signal<T>
|
|
52
|
+
function toSignal<T extends {}>(value: T): State<T>
|
|
53
|
+
function toSignal<T extends {}>(
|
|
54
|
+
value: MaybeSignal<T> | T[],
|
|
55
|
+
): Signal<T> | Store<Record<string, T>> {
|
|
56
|
+
if (isSignal<T>(value)) return value
|
|
57
|
+
if (isComputedCallback<T>(value)) return computed(value)
|
|
58
|
+
if (Array.isArray(value)) return store(arrayToRecord(value))
|
|
59
|
+
if (isRecord(value)) return store(value as T)
|
|
60
|
+
return state(value as T)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert a value to a mutable Signal if it's not already a Signal
|
|
65
|
+
*
|
|
66
|
+
* @since 0.9.6
|
|
67
|
+
*/
|
|
68
|
+
function toMutableSignal<T extends Array<unknown & {}>>(
|
|
69
|
+
value: T[],
|
|
70
|
+
): Store<Record<string, T>>
|
|
71
|
+
function toMutableSignal<T extends Record<keyof T, T[keyof T]>>(
|
|
72
|
+
value: T,
|
|
73
|
+
): Store<T>
|
|
74
|
+
function toMutableSignal<T extends State<T>>(value: State<T>): State<T>
|
|
75
|
+
function toMutableSignal<T extends Store<T>>(value: Store<T>): Store<T>
|
|
76
|
+
function toMutableSignal<T extends {}>(value: T): State<T>
|
|
77
|
+
function toMutableSignal<T extends {}>(
|
|
78
|
+
value: T | State<T> | Store<T> | T[],
|
|
79
|
+
): Signal<T> | Store<Record<string, T>> {
|
|
80
|
+
if (isState<T>(value) || isStore<T>(value)) return value
|
|
81
|
+
if (Array.isArray(value)) return store(arrayToRecord(value))
|
|
82
|
+
if (isRecord(value)) return store(value as T)
|
|
83
|
+
return state(value as T)
|
|
84
|
+
}
|
|
52
85
|
|
|
53
86
|
/* === Exports === */
|
|
54
87
|
|
|
@@ -58,6 +91,6 @@ export {
|
|
|
58
91
|
type SignalValues,
|
|
59
92
|
UNSET,
|
|
60
93
|
isSignal,
|
|
61
|
-
isComputedCallback,
|
|
62
94
|
toSignal,
|
|
95
|
+
toMutableSignal,
|
|
63
96
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isEqual } from './diff'
|
|
2
|
+
import { notify, subscribe, type Watcher } from './scheduler'
|
|
1
3
|
import { UNSET } from './signal'
|
|
2
4
|
import { isObjectOfType } from './util'
|
|
3
|
-
import { type Watcher, notify, subscribe } from './scheduler'
|
|
4
5
|
|
|
5
6
|
/* === Types === */
|
|
6
7
|
|
|
@@ -50,7 +51,7 @@ const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
|
|
|
50
51
|
* @returns {void}
|
|
51
52
|
*/
|
|
52
53
|
set: (v: T): void => {
|
|
53
|
-
if (
|
|
54
|
+
if (isEqual(value, v)) return
|
|
54
55
|
value = v
|
|
55
56
|
notify(watchers)
|
|
56
57
|
|
|
@@ -86,4 +87,4 @@ const isState = /*#__PURE__*/ <T extends {}>(
|
|
|
86
87
|
|
|
87
88
|
/* === Exports === */
|
|
88
89
|
|
|
89
|
-
export {
|
|
90
|
+
export { TYPE_STATE, isState, state, type State }
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { diff, type UnknownRecord } from './diff'
|
|
2
|
+
import { effect } from './effect'
|
|
3
|
+
import {
|
|
4
|
+
batch,
|
|
5
|
+
type Cleanup,
|
|
6
|
+
notify,
|
|
7
|
+
subscribe,
|
|
8
|
+
type Watcher,
|
|
9
|
+
} from './scheduler'
|
|
10
|
+
import { type Signal, toMutableSignal, UNSET } from './signal'
|
|
11
|
+
import { type State, state } from './state'
|
|
12
|
+
import { hasMethod, isObjectOfType } from './util'
|
|
13
|
+
|
|
14
|
+
/* === Constants === */
|
|
15
|
+
|
|
16
|
+
const TYPE_STORE = 'Store'
|
|
17
|
+
|
|
18
|
+
/* === Types === */
|
|
19
|
+
|
|
20
|
+
interface StoreAddEvent<T extends UnknownRecord> extends CustomEvent {
|
|
21
|
+
type: 'store-add'
|
|
22
|
+
detail: Partial<T>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface StoreChangeEvent<T extends UnknownRecord> extends CustomEvent {
|
|
26
|
+
type: 'store-change'
|
|
27
|
+
detail: Partial<T>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StoreRemoveEvent<T extends UnknownRecord> extends CustomEvent {
|
|
31
|
+
type: 'store-remove'
|
|
32
|
+
detail: Partial<T>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type StoreEventMap<T extends UnknownRecord> = {
|
|
36
|
+
'store-add': StoreAddEvent<T>
|
|
37
|
+
'store-change': StoreChangeEvent<T>
|
|
38
|
+
'store-remove': StoreRemoveEvent<T>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface StoreEventTarget<T extends UnknownRecord> extends EventTarget {
|
|
42
|
+
addEventListener<K extends keyof StoreEventMap<T>>(
|
|
43
|
+
type: K,
|
|
44
|
+
listener: (event: StoreEventMap<T>[K]) => void,
|
|
45
|
+
options?: boolean | AddEventListenerOptions,
|
|
46
|
+
): void
|
|
47
|
+
addEventListener(
|
|
48
|
+
type: string,
|
|
49
|
+
listener: EventListenerOrEventListenerObject,
|
|
50
|
+
options?: boolean | AddEventListenerOptions,
|
|
51
|
+
): void
|
|
52
|
+
|
|
53
|
+
removeEventListener<K extends keyof StoreEventMap<T>>(
|
|
54
|
+
type: K,
|
|
55
|
+
listener: (event: StoreEventMap<T>[K]) => void,
|
|
56
|
+
options?: boolean | EventListenerOptions,
|
|
57
|
+
): void
|
|
58
|
+
removeEventListener(
|
|
59
|
+
type: string,
|
|
60
|
+
listener: EventListenerOrEventListenerObject,
|
|
61
|
+
options?: boolean | EventListenerOptions,
|
|
62
|
+
): void
|
|
63
|
+
|
|
64
|
+
dispatchEvent(event: Event): boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Store<T extends UnknownRecord = UnknownRecord> = {
|
|
68
|
+
[K in keyof T & string]: T[K] extends UnknownRecord
|
|
69
|
+
? Store<T[K]>
|
|
70
|
+
: State<T[K]>
|
|
71
|
+
} & StoreEventTarget<T> & {
|
|
72
|
+
[Symbol.toStringTag]: 'Store'
|
|
73
|
+
[Symbol.iterator](): IterableIterator<[string, Signal<T[keyof T]>]>
|
|
74
|
+
|
|
75
|
+
// Signal methods
|
|
76
|
+
add<K extends keyof T & string>(key: K, value: T[K]): void
|
|
77
|
+
get(): T
|
|
78
|
+
remove<K extends keyof T & string>(key: K): void
|
|
79
|
+
set(value: T): void
|
|
80
|
+
update(updater: (value: T) => T): void
|
|
81
|
+
|
|
82
|
+
// Interals signals
|
|
83
|
+
size: State<number>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* === Functions === */
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a new store with deeply nested reactive properties
|
|
90
|
+
*
|
|
91
|
+
* @since 0.15.0
|
|
92
|
+
* @param {T} initialValue - initial object value of the store
|
|
93
|
+
* @returns {Store<T>} - new store with reactive properties
|
|
94
|
+
*/
|
|
95
|
+
const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
|
|
96
|
+
const watchers: Set<Watcher> = new Set()
|
|
97
|
+
const eventTarget = new EventTarget()
|
|
98
|
+
const signals: Map<
|
|
99
|
+
keyof T & string,
|
|
100
|
+
Store<T[keyof T & string]> | State<T[keyof T & string]>
|
|
101
|
+
> = new Map()
|
|
102
|
+
const cleanups = new Map<keyof T & string, Cleanup>()
|
|
103
|
+
|
|
104
|
+
// Internal state
|
|
105
|
+
const size = state(0)
|
|
106
|
+
|
|
107
|
+
// Get current record
|
|
108
|
+
const current = () => {
|
|
109
|
+
const record: Partial<T> = {}
|
|
110
|
+
for (const [key, value] of signals) {
|
|
111
|
+
record[key] = value.get()
|
|
112
|
+
}
|
|
113
|
+
return record as T
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Emit event
|
|
117
|
+
const emit = (type: keyof StoreEventMap<T>, detail: Partial<T>) =>
|
|
118
|
+
eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
|
|
119
|
+
|
|
120
|
+
// Add nested signal and effect
|
|
121
|
+
const addSignalAndEffect = <K extends keyof T & string>(
|
|
122
|
+
key: K,
|
|
123
|
+
value: T[K],
|
|
124
|
+
) => {
|
|
125
|
+
const signal = toMutableSignal<T[keyof T & string]>(value)
|
|
126
|
+
signals.set(key, signal)
|
|
127
|
+
const cleanup = effect(() => {
|
|
128
|
+
const value = signal.get()
|
|
129
|
+
if (value != null)
|
|
130
|
+
emit('store-change', { [key]: value } as unknown as Partial<T>)
|
|
131
|
+
})
|
|
132
|
+
cleanups.set(key, cleanup)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Remove nested signal and effect
|
|
136
|
+
const removeSignalAndEffect = <K extends keyof T & string>(key: K) => {
|
|
137
|
+
signals.delete(key)
|
|
138
|
+
const cleanup = cleanups.get(key)
|
|
139
|
+
if (cleanup) cleanup()
|
|
140
|
+
cleanups.delete(key)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Reconcile data and dispatch events
|
|
144
|
+
const reconcile = (oldValue: T, newValue: T): boolean => {
|
|
145
|
+
const changes = diff(oldValue, newValue)
|
|
146
|
+
|
|
147
|
+
batch(() => {
|
|
148
|
+
if (Object.keys(changes.add).length) {
|
|
149
|
+
for (const key in changes.add) {
|
|
150
|
+
const value = changes.add[key]
|
|
151
|
+
if (value != null) addSignalAndEffect(key, value)
|
|
152
|
+
}
|
|
153
|
+
emit('store-add', changes.add)
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(changes.change).length) {
|
|
156
|
+
for (const key in changes.change) {
|
|
157
|
+
const signal = signals.get(key as keyof T & string)
|
|
158
|
+
const value = changes.change[key]
|
|
159
|
+
if (
|
|
160
|
+
signal &&
|
|
161
|
+
value != null &&
|
|
162
|
+
hasMethod<Signal<T[keyof T & string]>>(signal, 'set')
|
|
163
|
+
)
|
|
164
|
+
signal.set(value)
|
|
165
|
+
}
|
|
166
|
+
emit('store-change', changes.change)
|
|
167
|
+
}
|
|
168
|
+
if (Object.keys(changes.remove).length) {
|
|
169
|
+
for (const key in changes.remove) {
|
|
170
|
+
removeSignalAndEffect(key)
|
|
171
|
+
}
|
|
172
|
+
emit('store-remove', changes.remove)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
size.set(signals.size)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return changes.changed
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Initialize data
|
|
182
|
+
reconcile({} as T, initialValue)
|
|
183
|
+
|
|
184
|
+
// Queue initial additions event to allow listeners to be added first
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
const initialAdditionsEvent = new CustomEvent('store-add', {
|
|
187
|
+
detail: initialValue as Partial<T>,
|
|
188
|
+
}) as StoreAddEvent<T>
|
|
189
|
+
eventTarget.dispatchEvent(initialAdditionsEvent)
|
|
190
|
+
}, 0)
|
|
191
|
+
|
|
192
|
+
const storeProps = [
|
|
193
|
+
'add',
|
|
194
|
+
'get',
|
|
195
|
+
'remove',
|
|
196
|
+
'set',
|
|
197
|
+
'update',
|
|
198
|
+
'addEventListener',
|
|
199
|
+
'removeEventListener',
|
|
200
|
+
'dispatchEvent',
|
|
201
|
+
'size',
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
// Return proxy directly with integrated signal methods
|
|
205
|
+
return new Proxy({} as Store<T>, {
|
|
206
|
+
get(_target, prop) {
|
|
207
|
+
const key = String(prop)
|
|
208
|
+
|
|
209
|
+
// Handle signal methods and size property
|
|
210
|
+
switch (prop) {
|
|
211
|
+
case 'add':
|
|
212
|
+
return <K extends keyof T & string>(
|
|
213
|
+
k: K,
|
|
214
|
+
v: T[K],
|
|
215
|
+
): void => {
|
|
216
|
+
if (!signals.has(k)) {
|
|
217
|
+
addSignalAndEffect(k, v)
|
|
218
|
+
notify(watchers)
|
|
219
|
+
emit('store-add', {
|
|
220
|
+
[k]: v,
|
|
221
|
+
} as unknown as Partial<T>)
|
|
222
|
+
size.set(signals.size)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
case 'get':
|
|
226
|
+
return (): T => {
|
|
227
|
+
subscribe(watchers)
|
|
228
|
+
return current()
|
|
229
|
+
}
|
|
230
|
+
case 'remove':
|
|
231
|
+
return <K extends keyof T & string>(k: K): void => {
|
|
232
|
+
if (signals.has(k)) {
|
|
233
|
+
removeSignalAndEffect(k)
|
|
234
|
+
notify(watchers)
|
|
235
|
+
emit('store-remove', { [k]: UNSET } as Partial<T>)
|
|
236
|
+
size.set(signals.size)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
case 'set':
|
|
240
|
+
return (v: T): void => {
|
|
241
|
+
if (reconcile(current(), v)) {
|
|
242
|
+
notify(watchers)
|
|
243
|
+
if (UNSET === v) watchers.clear()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
case 'update':
|
|
247
|
+
return (fn: (v: T) => T): void => {
|
|
248
|
+
const oldValue = current()
|
|
249
|
+
const newValue = fn(oldValue)
|
|
250
|
+
if (reconcile(oldValue, newValue)) {
|
|
251
|
+
notify(watchers)
|
|
252
|
+
if (UNSET === newValue) watchers.clear()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
case 'addEventListener':
|
|
256
|
+
return eventTarget.addEventListener.bind(eventTarget)
|
|
257
|
+
case 'removeEventListener':
|
|
258
|
+
return eventTarget.removeEventListener.bind(eventTarget)
|
|
259
|
+
case 'dispatchEvent':
|
|
260
|
+
return eventTarget.dispatchEvent.bind(eventTarget)
|
|
261
|
+
case 'size':
|
|
262
|
+
return size
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Handle symbol properties
|
|
266
|
+
if (prop === Symbol.toStringTag) return TYPE_STORE
|
|
267
|
+
if (prop === Symbol.iterator) {
|
|
268
|
+
return function* () {
|
|
269
|
+
for (const [key, signal] of signals) {
|
|
270
|
+
yield [key, signal as Signal<T[keyof T]>]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle data properties - return signals
|
|
276
|
+
return signals.get(key)
|
|
277
|
+
},
|
|
278
|
+
has(_target, prop) {
|
|
279
|
+
const key = String(prop)
|
|
280
|
+
return (
|
|
281
|
+
signals.has(key) ||
|
|
282
|
+
storeProps.includes(key) ||
|
|
283
|
+
prop === Symbol.toStringTag ||
|
|
284
|
+
prop === Symbol.iterator
|
|
285
|
+
)
|
|
286
|
+
},
|
|
287
|
+
ownKeys() {
|
|
288
|
+
return Array.from(signals.keys())
|
|
289
|
+
},
|
|
290
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
291
|
+
const signal = signals.get(String(prop))
|
|
292
|
+
return signal
|
|
293
|
+
? {
|
|
294
|
+
enumerable: true,
|
|
295
|
+
configurable: true,
|
|
296
|
+
writable: true,
|
|
297
|
+
value: signal,
|
|
298
|
+
}
|
|
299
|
+
: undefined
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Check if the provided value is a Store instance
|
|
306
|
+
*
|
|
307
|
+
* @since 0.15.0
|
|
308
|
+
* @param {unknown} value - value to check
|
|
309
|
+
* @returns {boolean} - true if the value is a Store instance, false otherwise
|
|
310
|
+
*/
|
|
311
|
+
const isStore = <T extends UnknownRecord>(value: unknown): value is Store<T> =>
|
|
312
|
+
isObjectOfType(value, TYPE_STORE)
|
|
313
|
+
|
|
314
|
+
/* === Exports === */
|
|
315
|
+
|
|
316
|
+
export {
|
|
317
|
+
TYPE_STORE,
|
|
318
|
+
isStore,
|
|
319
|
+
store,
|
|
320
|
+
type Store,
|
|
321
|
+
type StoreAddEvent,
|
|
322
|
+
type StoreChangeEvent,
|
|
323
|
+
type StoreRemoveEvent,
|
|
324
|
+
type StoreEventMap,
|
|
325
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,24 +1,76 @@
|
|
|
1
1
|
/* === Utility Functions === */
|
|
2
2
|
|
|
3
|
+
const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
|
|
4
|
+
typeof value === 'number'
|
|
5
|
+
|
|
6
|
+
const isString = /*#__PURE__*/ (value: unknown): value is string =>
|
|
7
|
+
typeof value === 'string'
|
|
8
|
+
|
|
3
9
|
const isFunction = /*#__PURE__*/ <T>(
|
|
4
|
-
|
|
5
|
-
):
|
|
10
|
+
fn: unknown,
|
|
11
|
+
): fn is (...args: unknown[]) => T => typeof fn === 'function'
|
|
12
|
+
|
|
13
|
+
const isAsyncFunction = /*#__PURE__*/ <T>(
|
|
14
|
+
fn: unknown,
|
|
15
|
+
): fn is (...args: unknown[]) => Promise<T> =>
|
|
16
|
+
isFunction(fn) && fn.constructor.name === 'AsyncFunction'
|
|
6
17
|
|
|
7
18
|
const isObjectOfType = /*#__PURE__*/ <T>(
|
|
8
19
|
value: unknown,
|
|
9
20
|
type: string,
|
|
10
21
|
): value is T => Object.prototype.toString.call(value) === `[object ${type}]`
|
|
11
22
|
|
|
12
|
-
const
|
|
23
|
+
const isRecord = /*#__PURE__*/ <T extends Record<string, unknown>>(
|
|
24
|
+
value: unknown,
|
|
25
|
+
): value is T => isObjectOfType(value, 'Object')
|
|
26
|
+
|
|
27
|
+
const isPrimitive = /*#__PURE__*/ (value: unknown): boolean =>
|
|
28
|
+
typeof value !== 'object' && !isFunction(value)
|
|
29
|
+
|
|
30
|
+
const arrayToRecord = /*#__PURE__*/ <T extends unknown & {}>(
|
|
31
|
+
array: T[],
|
|
32
|
+
): Record<string, T> => {
|
|
33
|
+
const record: Record<string, T> = {}
|
|
34
|
+
for (let i = 0; i < array.length; i++) {
|
|
35
|
+
if (i in array) record[String(i)] = array[i]
|
|
36
|
+
}
|
|
37
|
+
return record
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hasMethod = /*#__PURE__*/ <
|
|
41
|
+
T extends object & Record<string, (...args: unknown[]) => unknown>,
|
|
42
|
+
>(
|
|
43
|
+
obj: T,
|
|
44
|
+
methodName: string,
|
|
45
|
+
): obj is T & Record<string, (...args: unknown[]) => unknown> =>
|
|
46
|
+
methodName in obj && isFunction(obj[methodName])
|
|
47
|
+
|
|
48
|
+
const isAbortError = /*#__PURE__*/ (error: unknown): boolean =>
|
|
49
|
+
error instanceof DOMException && error.name === 'AbortError'
|
|
50
|
+
|
|
51
|
+
const toError = /*#__PURE__*/ (reason: unknown): Error =>
|
|
13
52
|
reason instanceof Error ? reason : Error(String(reason))
|
|
14
53
|
|
|
15
54
|
class CircularDependencyError extends Error {
|
|
16
55
|
constructor(where: string) {
|
|
17
56
|
super(`Circular dependency in ${where} detected`)
|
|
18
|
-
|
|
57
|
+
this.name = 'CircularDependencyError'
|
|
19
58
|
}
|
|
20
59
|
}
|
|
21
60
|
|
|
22
61
|
/* === Exports === */
|
|
23
62
|
|
|
24
|
-
export {
|
|
63
|
+
export {
|
|
64
|
+
isNumber,
|
|
65
|
+
isString,
|
|
66
|
+
isFunction,
|
|
67
|
+
isAsyncFunction,
|
|
68
|
+
isObjectOfType,
|
|
69
|
+
isRecord,
|
|
70
|
+
isPrimitive,
|
|
71
|
+
arrayToRecord,
|
|
72
|
+
hasMethod,
|
|
73
|
+
isAbortError,
|
|
74
|
+
toError,
|
|
75
|
+
CircularDependencyError,
|
|
76
|
+
}
|
package/test/batch.test.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { batch, computed, effect, match, resolve, state } from '../'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
|
-
describe('Batch',
|
|
7
|
-
test('should be triggered only once after repeated state change',
|
|
6
|
+
describe('Batch', () => {
|
|
7
|
+
test('should be triggered only once after repeated state change', () => {
|
|
8
8
|
const cause = state(0)
|
|
9
9
|
let result = 0
|
|
10
10
|
let count = 0
|
|
11
|
-
effect(() => {
|
|
11
|
+
effect((): undefined => {
|
|
12
12
|
result = cause.get()
|
|
13
13
|
count++
|
|
14
14
|
})
|
|
@@ -21,20 +21,22 @@ describe('Batch', function () {
|
|
|
21
21
|
expect(count).toBe(2) // + 1 for effect initialization
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
test('should be triggered only once when multiple signals are set',
|
|
24
|
+
test('should be triggered only once when multiple signals are set', () => {
|
|
25
25
|
const a = state(3)
|
|
26
26
|
const b = state(4)
|
|
27
27
|
const c = state(5)
|
|
28
28
|
const sum = computed(() => a.get() + b.get() + c.get())
|
|
29
29
|
let result = 0
|
|
30
30
|
let count = 0
|
|
31
|
-
effect({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
effect(() => {
|
|
32
|
+
const resolved = resolve({ sum })
|
|
33
|
+
match(resolved, {
|
|
34
|
+
ok: ({ sum: res }) => {
|
|
35
|
+
result = res
|
|
36
|
+
count++
|
|
37
|
+
},
|
|
38
|
+
err: () => {},
|
|
39
|
+
})
|
|
38
40
|
})
|
|
39
41
|
batch(() => {
|
|
40
42
|
a.set(6)
|
|
@@ -45,7 +47,7 @@ describe('Batch', function () {
|
|
|
45
47
|
expect(count).toBe(2) // + 1 for effect initialization
|
|
46
48
|
})
|
|
47
49
|
|
|
48
|
-
test('should prove example from README works',
|
|
50
|
+
test('should prove example from README works', () => {
|
|
49
51
|
// State: define an array of Signal<number>
|
|
50
52
|
const signals = [state(2), state(3), state(5)]
|
|
51
53
|
|
|
@@ -61,17 +63,19 @@ describe('Batch', function () {
|
|
|
61
63
|
let errCount = 0
|
|
62
64
|
|
|
63
65
|
// Effect: switch cases for the result
|
|
64
|
-
effect({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
effect(() => {
|
|
67
|
+
const resolved = resolve({ sum })
|
|
68
|
+
match(resolved, {
|
|
69
|
+
ok: ({ sum: v }) => {
|
|
70
|
+
result = v
|
|
71
|
+
okCount++
|
|
72
|
+
// console.log('Sum:', v)
|
|
73
|
+
},
|
|
74
|
+
err: () => {
|
|
75
|
+
errCount++
|
|
76
|
+
// console.error('Error:', error)
|
|
77
|
+
},
|
|
78
|
+
})
|
|
75
79
|
})
|
|
76
80
|
|
|
77
81
|
expect(okCount).toBe(1)
|