@zeix/cause-effect 0.15.2 → 0.16.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 +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +136 -166
- package/eslint.config.js +1 -1
- package/index.dev.js +125 -129
- package/index.js +1 -1
- package/index.ts +22 -22
- package/package.json +1 -1
- package/src/computed.ts +40 -29
- package/src/effect.ts +15 -12
- package/src/errors.ts +8 -0
- package/src/signal.ts +6 -6
- package/src/state.ts +27 -20
- package/src/store.ts +99 -121
- package/src/system.ts +122 -0
- package/src/util.ts +1 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +507 -71
- package/test/effect.test.ts +60 -60
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +7 -7
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +476 -183
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +8 -8
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +4 -1
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +27 -41
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +1 -2
- package/src/scheduler.ts +0 -172
package/src/store.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type UnknownRecord,
|
|
7
7
|
type UnknownRecordOrArray,
|
|
8
8
|
} from './diff'
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import {
|
|
11
11
|
InvalidSignalValueError,
|
|
12
12
|
NullishSignalValueError,
|
|
@@ -14,15 +14,17 @@ import {
|
|
|
14
14
|
StoreKeyRangeError,
|
|
15
15
|
StoreKeyReadonlyError,
|
|
16
16
|
} from './errors'
|
|
17
|
+
import { isMutableSignal, type Signal } from './signal'
|
|
18
|
+
import { createState, isState, type State } from './state'
|
|
17
19
|
import {
|
|
18
20
|
batch,
|
|
19
21
|
type Cleanup,
|
|
22
|
+
createWatcher,
|
|
20
23
|
notify,
|
|
24
|
+
observe,
|
|
21
25
|
subscribe,
|
|
22
26
|
type Watcher,
|
|
23
|
-
} from './
|
|
24
|
-
import { isMutableSignal, type Signal } from './signal'
|
|
25
|
-
import { isState, type State, state } from './state'
|
|
27
|
+
} from './system'
|
|
26
28
|
import {
|
|
27
29
|
isFunction,
|
|
28
30
|
isObjectOfType,
|
|
@@ -37,51 +39,27 @@ import {
|
|
|
37
39
|
|
|
38
40
|
type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
|
|
39
41
|
|
|
40
|
-
type
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
type StoreChanges<T> = {
|
|
43
|
+
add: Partial<T>
|
|
44
|
+
change: Partial<T>
|
|
45
|
+
remove: Partial<T>
|
|
46
|
+
sort: string[]
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
addEventListener<K extends keyof StoreEventMap<T>>(
|
|
50
|
-
type: K,
|
|
51
|
-
listener: (event: StoreEventMap<T>[K]) => void,
|
|
52
|
-
options?: boolean | AddEventListenerOptions,
|
|
53
|
-
): void
|
|
54
|
-
|
|
55
|
-
removeEventListener<K extends keyof StoreEventMap<T>>(
|
|
56
|
-
type: K,
|
|
57
|
-
listener: (event: StoreEventMap<T>[K]) => void,
|
|
58
|
-
options?: boolean | EventListenerOptions,
|
|
59
|
-
): void
|
|
60
|
-
|
|
61
|
-
dispatchEvent(event: Event): boolean
|
|
49
|
+
type StoreListeners<T> = {
|
|
50
|
+
[K in keyof StoreChanges<T>]: Set<(change: StoreChanges<T>[K]) => void>
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
interface BaseStore
|
|
65
|
-
extends StoreEventTarget<T> {
|
|
53
|
+
interface BaseStore {
|
|
66
54
|
readonly [Symbol.toStringTag]: 'Store'
|
|
67
|
-
get(): T
|
|
68
|
-
set(value: T): void
|
|
69
|
-
update(fn: (value: T) => T): void
|
|
70
|
-
sort<
|
|
71
|
-
U = T extends UnknownArray ? ArrayItem<T> : T[Extract<keyof T, string>],
|
|
72
|
-
>(
|
|
73
|
-
compareFn?: (a: U, b: U) => number,
|
|
74
|
-
): void
|
|
75
55
|
readonly size: State<number>
|
|
76
56
|
}
|
|
77
57
|
|
|
78
|
-
type RecordStore<T extends UnknownRecord> = BaseStore
|
|
58
|
+
type RecordStore<T extends UnknownRecord> = BaseStore & {
|
|
79
59
|
[K in keyof T]: T[K] extends readonly unknown[] | Record<string, unknown>
|
|
80
60
|
? Store<T[K]>
|
|
81
61
|
: State<T[K]>
|
|
82
62
|
} & {
|
|
83
|
-
add<K extends Extract<keyof T, string>>(key: K, value: T[K]): void
|
|
84
|
-
remove<K extends Extract<keyof T, string>>(key: K): void
|
|
85
63
|
[Symbol.iterator](): IterableIterator<
|
|
86
64
|
[
|
|
87
65
|
Extract<keyof T, string>,
|
|
@@ -92,49 +70,46 @@ type RecordStore<T extends UnknownRecord> = BaseStore<T> & {
|
|
|
92
70
|
: State<T[Extract<keyof T, string>]>,
|
|
93
71
|
]
|
|
94
72
|
>
|
|
73
|
+
add<K extends Extract<keyof T, string>>(key: K, value: T[K]): void
|
|
74
|
+
get(): T
|
|
75
|
+
set(value: T): void
|
|
76
|
+
update(fn: (value: T) => T): void
|
|
77
|
+
sort<U = T[Extract<keyof T, string>]>(
|
|
78
|
+
compareFn?: (a: U, b: U) => number,
|
|
79
|
+
): void
|
|
80
|
+
on<K extends keyof StoreChanges<T>>(
|
|
81
|
+
type: K,
|
|
82
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
83
|
+
): Cleanup
|
|
84
|
+
remove<K extends Extract<keyof T, string>>(key: K): void
|
|
95
85
|
}
|
|
96
86
|
|
|
97
|
-
type ArrayStore<T extends UnknownArray> = BaseStore
|
|
98
|
-
|
|
87
|
+
type ArrayStore<T extends UnknownArray> = BaseStore & {
|
|
88
|
+
[Symbol.iterator](): IterableIterator<
|
|
89
|
+
ArrayItem<T> extends readonly unknown[] | Record<string, unknown>
|
|
90
|
+
? Store<ArrayItem<T>>
|
|
91
|
+
: State<ArrayItem<T>>
|
|
92
|
+
>
|
|
93
|
+
readonly [Symbol.isConcatSpreadable]: boolean
|
|
99
94
|
[n: number]: ArrayItem<T> extends
|
|
100
95
|
| readonly unknown[]
|
|
101
96
|
| Record<string, unknown>
|
|
102
97
|
? Store<ArrayItem<T>>
|
|
103
98
|
: State<ArrayItem<T>>
|
|
104
99
|
add(value: ArrayItem<T>): void
|
|
100
|
+
get(): T
|
|
101
|
+
set(value: T): void
|
|
102
|
+
update(fn: (value: T) => T): void
|
|
103
|
+
sort<U = ArrayItem<T>>(compareFn?: (a: U, b: U) => number): void
|
|
104
|
+
on<K extends keyof StoreChanges<T>>(
|
|
105
|
+
type: K,
|
|
106
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
107
|
+
): Cleanup
|
|
105
108
|
remove(index: number): void
|
|
106
|
-
|
|
107
|
-
ArrayItem<T> extends readonly unknown[] | Record<string, unknown>
|
|
108
|
-
? Store<ArrayItem<T>>
|
|
109
|
-
: State<ArrayItem<T>>
|
|
110
|
-
>
|
|
111
|
-
readonly [Symbol.isConcatSpreadable]: boolean
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface StoreAddEvent<T extends UnknownRecord | UnknownArray>
|
|
115
|
-
extends CustomEvent {
|
|
116
|
-
type: 'store-add'
|
|
117
|
-
detail: Partial<T>
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface StoreChangeEvent<T extends UnknownRecord | UnknownArray>
|
|
121
|
-
extends CustomEvent {
|
|
122
|
-
type: 'store-change'
|
|
123
|
-
detail: Partial<T>
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface StoreRemoveEvent<T extends UnknownRecord | UnknownArray>
|
|
127
|
-
extends CustomEvent {
|
|
128
|
-
type: 'store-remove'
|
|
129
|
-
detail: Partial<T>
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface StoreSortEvent extends CustomEvent {
|
|
133
|
-
type: 'store-sort'
|
|
134
|
-
detail: string[]
|
|
109
|
+
readonly length: number
|
|
135
110
|
}
|
|
136
111
|
|
|
137
|
-
type Store<T> = T extends UnknownRecord
|
|
112
|
+
type Store<T extends UnknownRecord | UnknownArray> = T extends UnknownRecord
|
|
138
113
|
? RecordStore<T>
|
|
139
114
|
: T extends UnknownArray
|
|
140
115
|
? ArrayStore<T>
|
|
@@ -144,11 +119,6 @@ type Store<T> = T extends UnknownRecord
|
|
|
144
119
|
|
|
145
120
|
const TYPE_STORE = 'Store'
|
|
146
121
|
|
|
147
|
-
const STORE_EVENT_ADD = 'store-add'
|
|
148
|
-
const STORE_EVENT_CHANGE = 'store-change'
|
|
149
|
-
const STORE_EVENT_REMOVE = 'store-remove'
|
|
150
|
-
const STORE_EVENT_SORT = 'store-sort'
|
|
151
|
-
|
|
152
122
|
/* === Functions === */
|
|
153
123
|
|
|
154
124
|
/**
|
|
@@ -163,19 +133,26 @@ const STORE_EVENT_SORT = 'store-sort'
|
|
|
163
133
|
* @param {T} initialValue - initial object or array value of the store
|
|
164
134
|
* @returns {Store<T>} - new store with reactive properties that preserves the original type T
|
|
165
135
|
*/
|
|
166
|
-
const
|
|
136
|
+
const createStore = <T extends UnknownRecord | UnknownArray>(
|
|
167
137
|
initialValue: T,
|
|
168
138
|
): Store<T> => {
|
|
139
|
+
if (initialValue == null) throw new NullishSignalValueError('store')
|
|
140
|
+
|
|
169
141
|
const watchers = new Set<Watcher>()
|
|
170
|
-
const
|
|
142
|
+
const listeners: StoreListeners<T> = {
|
|
143
|
+
add: new Set<(change: Partial<T>) => void>(),
|
|
144
|
+
change: new Set<(change: Partial<T>) => void>(),
|
|
145
|
+
remove: new Set<(change: Partial<T>) => void>(),
|
|
146
|
+
sort: new Set<(change: string[]) => void>(),
|
|
147
|
+
}
|
|
171
148
|
const signals = new Map<string, Signal<T[Extract<keyof T, string>] & {}>>()
|
|
172
|
-
const
|
|
149
|
+
const signalWatchers = new Map<string, Watcher>()
|
|
173
150
|
|
|
174
151
|
// Determine if this is an array-like store at creation time
|
|
175
152
|
const isArrayLike = Array.isArray(initialValue)
|
|
176
153
|
|
|
177
154
|
// Internal state
|
|
178
|
-
const size =
|
|
155
|
+
const size = createState(0)
|
|
179
156
|
|
|
180
157
|
// Get current record
|
|
181
158
|
const current = () => {
|
|
@@ -186,9 +163,16 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
186
163
|
return record
|
|
187
164
|
}
|
|
188
165
|
|
|
189
|
-
// Emit
|
|
190
|
-
const emit = <
|
|
191
|
-
|
|
166
|
+
// Emit change notifications
|
|
167
|
+
const emit = <K extends keyof StoreChanges<T>>(
|
|
168
|
+
key: K,
|
|
169
|
+
changes: StoreChanges<T>[K],
|
|
170
|
+
) => {
|
|
171
|
+
Object.freeze(changes)
|
|
172
|
+
for (const listener of listeners[key]) {
|
|
173
|
+
listener(changes)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
192
176
|
|
|
193
177
|
// Get sorted indexes
|
|
194
178
|
const getSortedIndexes = () =>
|
|
@@ -223,26 +207,25 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
223
207
|
const signal =
|
|
224
208
|
isState(value) || isStore(value)
|
|
225
209
|
? value
|
|
226
|
-
: isRecord(value)
|
|
227
|
-
?
|
|
228
|
-
:
|
|
229
|
-
? store(value)
|
|
230
|
-
: state(value)
|
|
210
|
+
: isRecord(value) || Array.isArray(value)
|
|
211
|
+
? createStore(value)
|
|
212
|
+
: createState(value)
|
|
231
213
|
// @ts-expect-error non-matching signal types
|
|
232
214
|
signals.set(key, signal)
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
[key]: currentValue,
|
|
215
|
+
const watcher = createWatcher(() =>
|
|
216
|
+
observe(() => {
|
|
217
|
+
emit('change', {
|
|
218
|
+
[key]: signal.get(),
|
|
238
219
|
} as unknown as Partial<T>)
|
|
239
|
-
|
|
240
|
-
|
|
220
|
+
}, watcher),
|
|
221
|
+
)
|
|
222
|
+
watcher()
|
|
223
|
+
signalWatchers.set(key, watcher)
|
|
241
224
|
|
|
242
225
|
if (single) {
|
|
243
226
|
size.set(signals.size)
|
|
244
227
|
notify(watchers)
|
|
245
|
-
emit(
|
|
228
|
+
emit('add', {
|
|
246
229
|
[key]: value,
|
|
247
230
|
} as unknown as Partial<T>)
|
|
248
231
|
}
|
|
@@ -256,15 +239,15 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
256
239
|
) => {
|
|
257
240
|
const ok = signals.delete(key)
|
|
258
241
|
if (ok) {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
|
|
242
|
+
const watcher = signalWatchers.get(key)
|
|
243
|
+
if (watcher) watcher.cleanup()
|
|
244
|
+
signalWatchers.delete(key)
|
|
262
245
|
}
|
|
263
246
|
|
|
264
247
|
if (single) {
|
|
265
248
|
size.set(signals.size)
|
|
266
249
|
notify(watchers)
|
|
267
|
-
emit(
|
|
250
|
+
emit('remove', {
|
|
268
251
|
[key]: UNSET,
|
|
269
252
|
} as unknown as Partial<T>)
|
|
270
253
|
}
|
|
@@ -296,10 +279,10 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
296
279
|
// Queue initial additions event to allow listeners to be added first
|
|
297
280
|
if (initialRun) {
|
|
298
281
|
setTimeout(() => {
|
|
299
|
-
emit(
|
|
282
|
+
emit('add', changes.add as Partial<T>)
|
|
300
283
|
}, 0)
|
|
301
284
|
} else {
|
|
302
|
-
emit
|
|
285
|
+
emit('add', changes.add as Partial<T>)
|
|
303
286
|
}
|
|
304
287
|
}
|
|
305
288
|
|
|
@@ -314,14 +297,14 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
314
297
|
else
|
|
315
298
|
throw new StoreKeyReadonlyError(key, valueString(value))
|
|
316
299
|
}
|
|
317
|
-
emit(
|
|
300
|
+
emit('change', changes.change as Partial<T>)
|
|
318
301
|
}
|
|
319
302
|
|
|
320
303
|
// Removals
|
|
321
304
|
if (Object.keys(changes.remove).length) {
|
|
322
305
|
for (const key in changes.remove)
|
|
323
306
|
removeProperty(key as Extract<keyof T, string>)
|
|
324
|
-
emit(
|
|
307
|
+
emit('remove', changes.remove as Partial<T>)
|
|
325
308
|
}
|
|
326
309
|
|
|
327
310
|
size.set(signals.size)
|
|
@@ -334,7 +317,7 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
334
317
|
reconcile({} as T, initialValue, true)
|
|
335
318
|
|
|
336
319
|
// Methods and Properties
|
|
337
|
-
const
|
|
320
|
+
const store: Record<string, unknown> = {
|
|
338
321
|
add: isArrayLike
|
|
339
322
|
? (v: ArrayItem<T>): void => {
|
|
340
323
|
const nextIndex = signals.size
|
|
@@ -426,11 +409,15 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
426
409
|
signals.clear()
|
|
427
410
|
newSignals.forEach((signal, key) => signals.set(key, signal))
|
|
428
411
|
notify(watchers)
|
|
429
|
-
emit(
|
|
412
|
+
emit('sort', newOrder)
|
|
413
|
+
},
|
|
414
|
+
on: <K extends keyof StoreChanges<T>>(
|
|
415
|
+
type: K,
|
|
416
|
+
listener: (change: StoreChanges<T>[K]) => void,
|
|
417
|
+
): Cleanup => {
|
|
418
|
+
listeners[type].add(listener)
|
|
419
|
+
return () => listeners[type].delete(listener)
|
|
430
420
|
},
|
|
431
|
-
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
|
432
|
-
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
|
433
|
-
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
|
434
421
|
size,
|
|
435
422
|
}
|
|
436
423
|
|
|
@@ -458,7 +445,7 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
458
445
|
if (isSymbol(prop)) return undefined
|
|
459
446
|
|
|
460
447
|
// Methods and Properties
|
|
461
|
-
if (prop in
|
|
448
|
+
if (prop in store) return store[prop]
|
|
462
449
|
if (prop === 'length' && isArrayLike) {
|
|
463
450
|
subscribe(watchers)
|
|
464
451
|
return size.get()
|
|
@@ -472,7 +459,7 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
472
459
|
return (
|
|
473
460
|
(stringProp &&
|
|
474
461
|
signals.has(stringProp as Extract<keyof T, string>)) ||
|
|
475
|
-
Object.keys(
|
|
462
|
+
Object.keys(store).includes(stringProp) ||
|
|
476
463
|
prop === Symbol.toStringTag ||
|
|
477
464
|
prop === Symbol.iterator ||
|
|
478
465
|
prop === Symbol.isConcatSpreadable ||
|
|
@@ -506,7 +493,8 @@ const store = <T extends UnknownRecord | UnknownArray>(
|
|
|
506
493
|
if (prop === Symbol.toStringTag) return nonEnumerable(TYPE_STORE)
|
|
507
494
|
if (isSymbol(prop)) return undefined
|
|
508
495
|
|
|
509
|
-
if (Object.keys(
|
|
496
|
+
if (Object.keys(store).includes(prop))
|
|
497
|
+
return nonEnumerable(store[prop])
|
|
510
498
|
|
|
511
499
|
const signal = signals.get(prop as Extract<keyof T, string>)
|
|
512
500
|
return signal
|
|
@@ -534,14 +522,4 @@ const isStore = <T extends UnknownRecordOrArray>(
|
|
|
534
522
|
|
|
535
523
|
/* === Exports === */
|
|
536
524
|
|
|
537
|
-
export {
|
|
538
|
-
TYPE_STORE,
|
|
539
|
-
isStore,
|
|
540
|
-
store,
|
|
541
|
-
type Store,
|
|
542
|
-
type StoreAddEvent,
|
|
543
|
-
type StoreChangeEvent,
|
|
544
|
-
type StoreRemoveEvent,
|
|
545
|
-
type StoreSortEvent,
|
|
546
|
-
type StoreEventMap,
|
|
547
|
-
}
|
|
525
|
+
export { TYPE_STORE, isStore, createStore, type Store, type StoreChanges }
|
package/src/system.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* === Types === */
|
|
2
|
+
|
|
3
|
+
type Cleanup = () => void
|
|
4
|
+
|
|
5
|
+
type Watcher = {
|
|
6
|
+
(): void
|
|
7
|
+
unwatch(cleanup: Cleanup): void
|
|
8
|
+
cleanup(): void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* === Internal === */
|
|
12
|
+
|
|
13
|
+
// Currently active watcher
|
|
14
|
+
let activeWatcher: Watcher | undefined
|
|
15
|
+
|
|
16
|
+
// Pending queue for batched change notifications
|
|
17
|
+
const pendingWatchers = new Set<Watcher>()
|
|
18
|
+
let batchDepth = 0
|
|
19
|
+
|
|
20
|
+
/* === Functions === */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a watcher that can be used to observe changes to a signal
|
|
24
|
+
*
|
|
25
|
+
* @since 0.14.1
|
|
26
|
+
* @param {() => void} watch - Function to be called when the state changes
|
|
27
|
+
* @returns {Watcher} - Watcher object with off and cleanup methods
|
|
28
|
+
*/
|
|
29
|
+
const createWatcher = (watch: () => void): Watcher => {
|
|
30
|
+
const cleanups = new Set<Cleanup>()
|
|
31
|
+
const w = watch as Partial<Watcher>
|
|
32
|
+
w.unwatch = (cleanup: Cleanup) => {
|
|
33
|
+
cleanups.add(cleanup)
|
|
34
|
+
}
|
|
35
|
+
w.cleanup = () => {
|
|
36
|
+
for (const cleanup of cleanups) cleanup()
|
|
37
|
+
cleanups.clear()
|
|
38
|
+
}
|
|
39
|
+
return w as Watcher
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add active watcher to the Set of watchers
|
|
44
|
+
*
|
|
45
|
+
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
46
|
+
*/
|
|
47
|
+
const subscribe = (watchers: Set<Watcher>) => {
|
|
48
|
+
if (activeWatcher && !watchers.has(activeWatcher)) {
|
|
49
|
+
const watcher = activeWatcher
|
|
50
|
+
watcher.unwatch(() => {
|
|
51
|
+
watchers.delete(watcher)
|
|
52
|
+
})
|
|
53
|
+
watchers.add(watcher)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add watchers to the pending set of change notifications
|
|
59
|
+
*
|
|
60
|
+
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
61
|
+
*/
|
|
62
|
+
const notify = (watchers: Set<Watcher>) => {
|
|
63
|
+
for (const watcher of watchers) {
|
|
64
|
+
if (batchDepth) pendingWatchers.add(watcher)
|
|
65
|
+
else watcher()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Flush all pending changes to notify watchers
|
|
71
|
+
*/
|
|
72
|
+
const flush = () => {
|
|
73
|
+
while (pendingWatchers.size) {
|
|
74
|
+
const watchers = Array.from(pendingWatchers)
|
|
75
|
+
pendingWatchers.clear()
|
|
76
|
+
for (const watcher of watchers) watcher()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Batch multiple changes in a single signal graph and DOM update cycle
|
|
82
|
+
*
|
|
83
|
+
* @param {() => void} fn - function with multiple signal writes to be batched
|
|
84
|
+
*/
|
|
85
|
+
const batch = (fn: () => void) => {
|
|
86
|
+
batchDepth++
|
|
87
|
+
try {
|
|
88
|
+
fn()
|
|
89
|
+
} finally {
|
|
90
|
+
flush()
|
|
91
|
+
batchDepth--
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run a function in a reactive context
|
|
97
|
+
*
|
|
98
|
+
* @param {() => void} run - function to run the computation or effect
|
|
99
|
+
* @param {Watcher} watcher - function to be called when the state changes or undefined for temporary unwatching while inserting auto-hydrating DOM nodes that might read signals (e.g., web components)
|
|
100
|
+
*/
|
|
101
|
+
const observe = (run: () => void, watcher?: Watcher): void => {
|
|
102
|
+
const prev = activeWatcher
|
|
103
|
+
activeWatcher = watcher
|
|
104
|
+
try {
|
|
105
|
+
run()
|
|
106
|
+
} finally {
|
|
107
|
+
activeWatcher = prev
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* === Exports === */
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
type Cleanup,
|
|
115
|
+
type Watcher,
|
|
116
|
+
subscribe,
|
|
117
|
+
notify,
|
|
118
|
+
flush,
|
|
119
|
+
batch,
|
|
120
|
+
createWatcher,
|
|
121
|
+
observe,
|
|
122
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -23,10 +23,6 @@ const isAsyncFunction = /*#__PURE__*/ <T>(
|
|
|
23
23
|
): fn is (...args: unknown[]) => Promise<T> =>
|
|
24
24
|
isFunction(fn) && fn.constructor.name === 'AsyncFunction'
|
|
25
25
|
|
|
26
|
-
const isDefinedObject = /*#__PURE__*/ (
|
|
27
|
-
value: unknown,
|
|
28
|
-
): value is Record<string, unknown> => !!value && typeof value === 'object'
|
|
29
|
-
|
|
30
26
|
const isObjectOfType = /*#__PURE__*/ <T>(
|
|
31
27
|
value: unknown,
|
|
32
28
|
type: string,
|
|
@@ -92,7 +88,7 @@ const recordToArray = /*#__PURE__*/ <T>(
|
|
|
92
88
|
const valueString = /*#__PURE__*/ (value: unknown): string =>
|
|
93
89
|
isString(value)
|
|
94
90
|
? `"${value}"`
|
|
95
|
-
:
|
|
91
|
+
: !!value && typeof value === 'object'
|
|
96
92
|
? JSON.stringify(value)
|
|
97
93
|
: String(value)
|
|
98
94
|
|
|
@@ -105,7 +101,6 @@ export {
|
|
|
105
101
|
isSymbol,
|
|
106
102
|
isFunction,
|
|
107
103
|
isAsyncFunction,
|
|
108
|
-
isDefinedObject,
|
|
109
104
|
isObjectOfType,
|
|
110
105
|
isRecord,
|
|
111
106
|
isRecordOrArray,
|
package/test/batch.test.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
batch,
|
|
4
|
+
createComputed,
|
|
5
|
+
createEffect,
|
|
6
|
+
createState,
|
|
7
|
+
match,
|
|
8
|
+
resolve,
|
|
9
|
+
} from '../'
|
|
3
10
|
|
|
4
11
|
/* === Tests === */
|
|
5
12
|
|
|
6
13
|
describe('Batch', () => {
|
|
7
14
|
test('should be triggered only once after repeated state change', () => {
|
|
8
|
-
const cause =
|
|
15
|
+
const cause = createState(0)
|
|
9
16
|
let result = 0
|
|
10
17
|
let count = 0
|
|
11
|
-
|
|
18
|
+
createEffect((): undefined => {
|
|
12
19
|
result = cause.get()
|
|
13
20
|
count++
|
|
14
21
|
})
|
|
@@ -22,13 +29,13 @@ describe('Batch', () => {
|
|
|
22
29
|
})
|
|
23
30
|
|
|
24
31
|
test('should be triggered only once when multiple signals are set', () => {
|
|
25
|
-
const a =
|
|
26
|
-
const b =
|
|
27
|
-
const c =
|
|
28
|
-
const sum =
|
|
32
|
+
const a = createState(3)
|
|
33
|
+
const b = createState(4)
|
|
34
|
+
const c = createState(5)
|
|
35
|
+
const sum = createComputed(() => a.get() + b.get() + c.get())
|
|
29
36
|
let result = 0
|
|
30
37
|
let count = 0
|
|
31
|
-
|
|
38
|
+
createEffect(() => {
|
|
32
39
|
const resolved = resolve({ sum })
|
|
33
40
|
match(resolved, {
|
|
34
41
|
ok: ({ sum: res }) => {
|
|
@@ -49,10 +56,10 @@ describe('Batch', () => {
|
|
|
49
56
|
|
|
50
57
|
test('should prove example from README works', () => {
|
|
51
58
|
// State: define an array of Signal<number>
|
|
52
|
-
const signals = [
|
|
59
|
+
const signals = [createState(2), createState(3), createState(5)]
|
|
53
60
|
|
|
54
61
|
// Computed: derive a calculation ...
|
|
55
|
-
const sum =
|
|
62
|
+
const sum = createComputed(() => {
|
|
56
63
|
const v = signals.reduce((total, v) => total + v.get(), 0)
|
|
57
64
|
if (!Number.isFinite(v)) throw new Error('Invalid value')
|
|
58
65
|
return v
|
|
@@ -63,7 +70,7 @@ describe('Batch', () => {
|
|
|
63
70
|
let errCount = 0
|
|
64
71
|
|
|
65
72
|
// Effect: switch cases for the result
|
|
66
|
-
|
|
73
|
+
createEffect(() => {
|
|
67
74
|
const resolved = resolve({ sum })
|
|
68
75
|
match(resolved, {
|
|
69
76
|
ok: ({ sum: v }) => {
|
package/test/benchmark.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from 'bun:test'
|
|
2
|
-
import { batch,
|
|
2
|
+
import { batch, createComputed, createEffect, createState } from '../'
|
|
3
3
|
import { Counter, makeGraph, runGraph } from './util/dependency-graph'
|
|
4
4
|
import type { Computed, ReactiveFramework } from './util/reactive-framework'
|
|
5
5
|
|
|
@@ -15,19 +15,19 @@ const busy = () => {
|
|
|
15
15
|
const framework = {
|
|
16
16
|
name: 'Cause & Effect',
|
|
17
17
|
signal: <T extends {}>(initialValue: T) => {
|
|
18
|
-
const s =
|
|
18
|
+
const s = createState<T>(initialValue)
|
|
19
19
|
return {
|
|
20
20
|
write: (v: T) => s.set(v),
|
|
21
21
|
read: () => s.get(),
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
computed: <T extends {}>(fn: () => T) => {
|
|
25
|
-
const c =
|
|
25
|
+
const c = createComputed(fn)
|
|
26
26
|
return {
|
|
27
27
|
read: () => c.get(),
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
-
effect: (fn: () => undefined) =>
|
|
30
|
+
effect: (fn: () => undefined) => createEffect(fn),
|
|
31
31
|
withBatch: (fn: () => undefined) => batch(fn),
|
|
32
32
|
withBuild: <T>(fn: () => T) => fn(),
|
|
33
33
|
}
|