@zeix/cause-effect 0.13.2 → 0.14.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 +160 -130
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/computed.d.ts +15 -17
- package/src/computed.ts +26 -200
- package/src/effect.d.ts +9 -12
- package/src/effect.ts +54 -28
- package/src/memo.d.ts +13 -0
- package/src/memo.ts +91 -0
- package/src/scheduler.d.ts +15 -11
- package/src/scheduler.ts +32 -15
- package/src/signal.d.ts +6 -20
- package/src/signal.ts +34 -67
- package/src/state.d.ts +4 -7
- package/src/state.ts +9 -39
- package/src/task.d.ts +17 -0
- package/src/task.ts +153 -0
- package/src/util.ts +2 -0
- package/test/batch.test.ts +10 -14
- package/test/benchmark.test.ts +81 -69
- package/test/computed.test.ts +108 -123
- package/test/effect.test.ts +24 -16
- package/test/state.test.ts +2 -53
- package/test/util/dependency-graph.ts +2 -2
package/src/computed.ts
CHANGED
|
@@ -1,220 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
isAbortError,
|
|
5
|
-
isAsyncFunction,
|
|
6
|
-
isFunction,
|
|
7
|
-
isObjectOfType,
|
|
8
|
-
isPromise,
|
|
9
|
-
toError,
|
|
10
|
-
} from './util'
|
|
11
|
-
import { type Watcher, flush, notify, subscribe, watch } from './scheduler'
|
|
12
|
-
import { type TapMatcher, type EffectMatcher, effect } from './effect'
|
|
1
|
+
import { isAsyncFunction, isObjectOfType } from './util'
|
|
2
|
+
import { type MemoCallback, memo } from './memo'
|
|
3
|
+
import { type TaskCallback, task } from './task'
|
|
13
4
|
|
|
14
5
|
/* === Types === */
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
signals: S
|
|
18
|
-
abort?: AbortSignal
|
|
19
|
-
ok: (
|
|
20
|
-
...values: {
|
|
21
|
-
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
22
|
-
}
|
|
23
|
-
) => R | Promise<R>
|
|
24
|
-
err?: (...errors: Error[]) => R | Promise<R>
|
|
25
|
-
nil?: () => R | Promise<R>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type Computed<T extends {}> = {
|
|
7
|
+
type Computed<T extends {}> = {
|
|
29
8
|
[Symbol.toStringTag]: 'Computed'
|
|
30
9
|
get(): T
|
|
31
|
-
map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>
|
|
32
|
-
tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void
|
|
33
10
|
}
|
|
11
|
+
type ComputedCallback<T extends {} & { then?: void }> =
|
|
12
|
+
| TaskCallback<T>
|
|
13
|
+
| MemoCallback<T>
|
|
34
14
|
|
|
35
15
|
/* === Constants === */
|
|
36
16
|
|
|
37
17
|
const TYPE_COMPUTED = 'Computed'
|
|
38
18
|
|
|
39
|
-
/* ===
|
|
40
|
-
|
|
41
|
-
const isEquivalentError = /*#__PURE__*/ (
|
|
42
|
-
error1: Error,
|
|
43
|
-
error2: Error | undefined,
|
|
44
|
-
): boolean => {
|
|
45
|
-
if (!error2) return false
|
|
46
|
-
return error1.name === error2.name && error1.message === error2.message
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* === Computed Factory === */
|
|
19
|
+
/* === Functions === */
|
|
50
20
|
|
|
51
21
|
/**
|
|
52
22
|
* Create a derived signal from existing signals
|
|
53
23
|
*
|
|
24
|
+
* This function delegates to either memo() for synchronous computations
|
|
25
|
+
* or task() for asynchronous computations, providing better performance
|
|
26
|
+
* for each case.
|
|
27
|
+
*
|
|
54
28
|
* @since 0.9.0
|
|
55
|
-
* @param {
|
|
29
|
+
* @param {ComputedCallback<T>} fn - computation callback function
|
|
56
30
|
* @returns {Computed<T>} - Computed signal
|
|
57
31
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
): Computed<T> => {
|
|
61
|
-
const watchers: Set<Watcher> = new Set()
|
|
62
|
-
const m = isFunction(matcher)
|
|
63
|
-
? undefined
|
|
64
|
-
: ({
|
|
65
|
-
nil: () => UNSET,
|
|
66
|
-
err: (...errors: Error[]) => {
|
|
67
|
-
if (errors.length > 1) throw new AggregateError(errors)
|
|
68
|
-
else throw errors[0]
|
|
69
|
-
},
|
|
70
|
-
...matcher,
|
|
71
|
-
} as Required<ComputedMatcher<S, T>>)
|
|
72
|
-
const fn = (m ? m.ok : matcher) as ComputedCallback<T>
|
|
73
|
-
|
|
74
|
-
// Internal state
|
|
75
|
-
let value: T = UNSET
|
|
76
|
-
let error: Error | undefined
|
|
77
|
-
let dirty = true
|
|
78
|
-
let changed = false
|
|
79
|
-
let computing = false
|
|
80
|
-
let controller: AbortController | undefined
|
|
81
|
-
|
|
82
|
-
// Functions to update internal state
|
|
83
|
-
const ok = (v: T) => {
|
|
84
|
-
if (!Object.is(v, value)) {
|
|
85
|
-
value = v
|
|
86
|
-
dirty = false
|
|
87
|
-
error = undefined
|
|
88
|
-
changed = true
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const nil = () => {
|
|
92
|
-
changed = UNSET !== value
|
|
93
|
-
value = UNSET
|
|
94
|
-
error = undefined
|
|
95
|
-
}
|
|
96
|
-
const err = (e: unknown) => {
|
|
97
|
-
const newError = toError(e)
|
|
98
|
-
changed = !isEquivalentError(newError, error)
|
|
99
|
-
value = UNSET
|
|
100
|
-
error = newError
|
|
101
|
-
}
|
|
102
|
-
const resolve = (v: T) => {
|
|
103
|
-
computing = false
|
|
104
|
-
controller = undefined
|
|
105
|
-
ok(v)
|
|
106
|
-
if (changed) notify(watchers)
|
|
107
|
-
}
|
|
108
|
-
const reject = (e: unknown) => {
|
|
109
|
-
computing = false
|
|
110
|
-
controller = undefined
|
|
111
|
-
err(e)
|
|
112
|
-
if (changed) notify(watchers)
|
|
113
|
-
}
|
|
114
|
-
const abort = () => {
|
|
115
|
-
computing = false
|
|
116
|
-
controller = undefined
|
|
117
|
-
compute() // retry
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Called when notified from sources (push)
|
|
121
|
-
const mark = (() => {
|
|
122
|
-
dirty = true
|
|
123
|
-
controller?.abort('Aborted because source signal changed')
|
|
124
|
-
if (watchers.size) {
|
|
125
|
-
notify(watchers)
|
|
126
|
-
} else {
|
|
127
|
-
mark.cleanups.forEach((fn: () => void) => fn())
|
|
128
|
-
mark.cleanups.clear()
|
|
129
|
-
}
|
|
130
|
-
}) as Watcher
|
|
131
|
-
mark.cleanups = new Set()
|
|
132
|
-
|
|
133
|
-
// Called when requested by dependencies (pull)
|
|
134
|
-
const compute = () =>
|
|
135
|
-
watch(() => {
|
|
136
|
-
if (computing) throw new CircularDependencyError('computed')
|
|
137
|
-
changed = false
|
|
138
|
-
if (isAsyncFunction(fn)) {
|
|
139
|
-
if (controller) return value // return current value until promise resolves
|
|
140
|
-
controller = new AbortController()
|
|
141
|
-
if (m)
|
|
142
|
-
m.abort =
|
|
143
|
-
m.abort instanceof AbortSignal
|
|
144
|
-
? AbortSignal.any([m.abort, controller.signal])
|
|
145
|
-
: controller.signal
|
|
146
|
-
controller.signal.addEventListener('abort', abort, {
|
|
147
|
-
once: true,
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
let result: T | Promise<T>
|
|
151
|
-
computing = true
|
|
152
|
-
try {
|
|
153
|
-
result =
|
|
154
|
-
m && m.signals.length
|
|
155
|
-
? match<S, T>(m)
|
|
156
|
-
: fn(controller?.signal)
|
|
157
|
-
} catch (e) {
|
|
158
|
-
if (isAbortError(e)) nil()
|
|
159
|
-
else err(e)
|
|
160
|
-
computing = false
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
if (isPromise(result)) result.then(resolve, reject)
|
|
164
|
-
else if (null == result || UNSET === result) nil()
|
|
165
|
-
else ok(result)
|
|
166
|
-
computing = false
|
|
167
|
-
}, mark)
|
|
168
|
-
|
|
169
|
-
const c: Computed<T> = {
|
|
170
|
-
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get the current value of the computed
|
|
174
|
-
*
|
|
175
|
-
* @since 0.9.0
|
|
176
|
-
* @returns {T} - current value of the computed
|
|
177
|
-
*/
|
|
178
|
-
get: (): T => {
|
|
179
|
-
subscribe(watchers)
|
|
180
|
-
flush()
|
|
181
|
-
if (dirty) compute()
|
|
182
|
-
if (error) throw error
|
|
183
|
-
return value
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Create a computed signal from the current computed signal
|
|
188
|
-
*
|
|
189
|
-
* @since 0.9.0
|
|
190
|
-
* @param {((v: T) => U | Promise<U>)} fn - computed callback
|
|
191
|
-
* @returns {Computed<U>} - computed signal
|
|
192
|
-
*/
|
|
193
|
-
map: <U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U> =>
|
|
194
|
-
computed({
|
|
195
|
-
signals: [c],
|
|
196
|
-
ok: fn,
|
|
197
|
-
}),
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Case matching for the computed signal with effect callbacks
|
|
201
|
-
*
|
|
202
|
-
* @since 0.13.0
|
|
203
|
-
* @param {TapMatcher<T> | ((v: T) => void | (() => void))} matcher - tap matcher or effect callback
|
|
204
|
-
* @returns {() => void} - cleanup function for the effect
|
|
205
|
-
*/
|
|
206
|
-
tap: (
|
|
207
|
-
matcher: TapMatcher<T> | ((v: T) => void | (() => void)),
|
|
208
|
-
): (() => void) =>
|
|
209
|
-
effect({
|
|
210
|
-
signals: [c],
|
|
211
|
-
...(isFunction(matcher) ? { ok: matcher } : matcher),
|
|
212
|
-
} as EffectMatcher<[Computed<T>]>),
|
|
213
|
-
}
|
|
214
|
-
return c
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/* === Helper Functions === */
|
|
32
|
+
const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> =>
|
|
33
|
+
isAsyncFunction<T>(fn) ? task<T>(fn) : memo<T>(fn as MemoCallback<T>)
|
|
218
34
|
|
|
219
35
|
/**
|
|
220
36
|
* Check if a value is a computed state
|
|
@@ -223,6 +39,16 @@ export const computed = <T extends {}, S extends Signal<{}>[] = []>(
|
|
|
223
39
|
* @param {unknown} value - value to check
|
|
224
40
|
* @returns {boolean} - true if value is a computed state, false otherwise
|
|
225
41
|
*/
|
|
226
|
-
|
|
42
|
+
const isComputed = /*#__PURE__*/ <T extends {}>(
|
|
227
43
|
value: unknown,
|
|
228
44
|
): value is Computed<T> => isObjectOfType(value, TYPE_COMPUTED)
|
|
45
|
+
|
|
46
|
+
/* === Exports === */
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
type Computed,
|
|
50
|
+
type ComputedCallback,
|
|
51
|
+
TYPE_COMPUTED,
|
|
52
|
+
computed,
|
|
53
|
+
isComputed,
|
|
54
|
+
}
|
package/src/effect.d.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { type Signal } from './signal';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
err?: (error: Error) => void | (() => void);
|
|
5
|
-
nil?: () => void | (() => void);
|
|
6
|
-
};
|
|
7
|
-
export type EffectMatcher<S extends Signal<{}>[]> = {
|
|
2
|
+
import { type Cleanup } from './scheduler';
|
|
3
|
+
type EffectMatcher<S extends Signal<{}>[]> = {
|
|
8
4
|
signals: S;
|
|
9
5
|
ok: (...values: {
|
|
10
6
|
[K in keyof S]: S[K] extends Signal<infer T> ? T : never;
|
|
11
|
-
}) => void |
|
|
12
|
-
err?: (...errors: Error[]) => void |
|
|
13
|
-
nil?: () => void |
|
|
7
|
+
}) => void | Cleanup;
|
|
8
|
+
err?: (...errors: Error[]) => void | Cleanup;
|
|
9
|
+
nil?: () => void | Cleanup;
|
|
14
10
|
};
|
|
15
11
|
/**
|
|
16
12
|
* Define what happens when a reactive state changes
|
|
17
13
|
*
|
|
18
14
|
* @since 0.1.0
|
|
19
|
-
* @param {EffectMatcher<S> | (() => void |
|
|
20
|
-
* @returns {
|
|
15
|
+
* @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
|
|
16
|
+
* @returns {Cleanup} - cleanup function for the effect
|
|
21
17
|
*/
|
|
22
|
-
|
|
18
|
+
declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() => void | Cleanup)): Cleanup;
|
|
19
|
+
export { type EffectMatcher, effect };
|
package/src/effect.ts
CHANGED
|
@@ -1,38 +1,37 @@
|
|
|
1
|
-
import { type Signal,
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { type Signal, UNSET } from './signal'
|
|
2
|
+
import {
|
|
3
|
+
CircularDependencyError,
|
|
4
|
+
isFunction,
|
|
5
|
+
toError,
|
|
6
|
+
isAbortError,
|
|
7
|
+
} from './util'
|
|
8
|
+
import { watch, type Cleanup, type Watcher } from './scheduler'
|
|
4
9
|
|
|
5
10
|
/* === Types === */
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
ok: (value: T) => void | (() => void)
|
|
9
|
-
err?: (error: Error) => void | (() => void)
|
|
10
|
-
nil?: () => void | (() => void)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type EffectMatcher<S extends Signal<{}>[]> = {
|
|
12
|
+
type EffectMatcher<S extends Signal<{}>[]> = {
|
|
14
13
|
signals: S
|
|
15
14
|
ok: (
|
|
16
15
|
...values: {
|
|
17
16
|
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
18
17
|
}
|
|
19
|
-
) => void |
|
|
20
|
-
err?: (...errors: Error[]) => void |
|
|
21
|
-
nil?: () => void |
|
|
18
|
+
) => void | Cleanup
|
|
19
|
+
err?: (...errors: Error[]) => void | Cleanup
|
|
20
|
+
nil?: () => void | Cleanup
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/* ===
|
|
23
|
+
/* === Functions === */
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Define what happens when a reactive state changes
|
|
28
27
|
*
|
|
29
28
|
* @since 0.1.0
|
|
30
|
-
* @param {EffectMatcher<S> | (() => void |
|
|
31
|
-
* @returns {
|
|
29
|
+
* @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
|
|
30
|
+
* @returns {Cleanup} - cleanup function for the effect
|
|
32
31
|
*/
|
|
33
|
-
|
|
34
|
-
matcher: EffectMatcher<S> | (() => void |
|
|
35
|
-
):
|
|
32
|
+
function effect<S extends Signal<{}>[]>(
|
|
33
|
+
matcher: EffectMatcher<S> | (() => void | Cleanup),
|
|
34
|
+
): Cleanup {
|
|
36
35
|
const {
|
|
37
36
|
signals,
|
|
38
37
|
ok,
|
|
@@ -46,24 +45,51 @@ export function effect<S extends Signal<{}>[]>(
|
|
|
46
45
|
watch(() => {
|
|
47
46
|
if (running) throw new CircularDependencyError('effect')
|
|
48
47
|
running = true
|
|
49
|
-
let cleanup: void |
|
|
48
|
+
let cleanup: void | Cleanup = undefined
|
|
50
49
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
const errors: Error[] = []
|
|
51
|
+
let suspense = false
|
|
52
|
+
const values = signals.map(signal => {
|
|
53
|
+
try {
|
|
54
|
+
const value = signal.get()
|
|
55
|
+
if (value === UNSET) suspense = true
|
|
56
|
+
return value
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (isAbortError(e)) throw e
|
|
59
|
+
errors.push(toError(e))
|
|
60
|
+
return UNSET
|
|
61
|
+
}
|
|
62
|
+
}) as {
|
|
63
|
+
[K in keyof S]: S[K] extends Signal<infer T extends {}>
|
|
64
|
+
? T
|
|
65
|
+
: never
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
cleanup = suspense
|
|
70
|
+
? nil()
|
|
71
|
+
: errors.length
|
|
72
|
+
? err(...errors)
|
|
73
|
+
: ok(...values)
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (isAbortError(e)) throw e
|
|
76
|
+
const error = toError(e)
|
|
77
|
+
cleanup = err(error)
|
|
78
|
+
}
|
|
57
79
|
} catch (e) {
|
|
58
80
|
err(toError(e))
|
|
59
81
|
}
|
|
60
82
|
if (isFunction(cleanup)) run.cleanups.add(cleanup)
|
|
61
83
|
running = false
|
|
62
84
|
}, run)) as Watcher
|
|
63
|
-
run.cleanups = new Set()
|
|
85
|
+
run.cleanups = new Set<Cleanup>()
|
|
64
86
|
run()
|
|
65
87
|
return () => {
|
|
66
|
-
run.cleanups.forEach(
|
|
88
|
+
run.cleanups.forEach(fn => fn())
|
|
67
89
|
run.cleanups.clear()
|
|
68
90
|
}
|
|
69
91
|
}
|
|
92
|
+
|
|
93
|
+
/* === Exports === */
|
|
94
|
+
|
|
95
|
+
export { type EffectMatcher, effect }
|
package/src/memo.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Computed } from './computed';
|
|
2
|
+
type MemoCallback<T extends {} & {
|
|
3
|
+
then?: void;
|
|
4
|
+
}> = () => T;
|
|
5
|
+
/**
|
|
6
|
+
* Create a derived signal for synchronous computations
|
|
7
|
+
*
|
|
8
|
+
* @since 0.14.0
|
|
9
|
+
* @param {MemoCallback<T>} fn - synchronous computation callback
|
|
10
|
+
* @returns {Computed<T>} - Computed signal
|
|
11
|
+
*/
|
|
12
|
+
declare const memo: <T extends {}>(fn: MemoCallback<T>) => Computed<T>;
|
|
13
|
+
export { type MemoCallback, memo };
|
package/src/memo.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { UNSET } from './signal'
|
|
2
|
+
import { CircularDependencyError } from './util'
|
|
3
|
+
import {
|
|
4
|
+
type Cleanup,
|
|
5
|
+
type Watcher,
|
|
6
|
+
flush,
|
|
7
|
+
notify,
|
|
8
|
+
subscribe,
|
|
9
|
+
watch,
|
|
10
|
+
} from './scheduler'
|
|
11
|
+
import { type Computed, TYPE_COMPUTED } from './computed'
|
|
12
|
+
|
|
13
|
+
/* === Types === */
|
|
14
|
+
|
|
15
|
+
type MemoCallback<T extends {} & { then?: void }> = () => T
|
|
16
|
+
|
|
17
|
+
/* === Functions === */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a derived signal for synchronous computations
|
|
21
|
+
*
|
|
22
|
+
* @since 0.14.0
|
|
23
|
+
* @param {MemoCallback<T>} fn - synchronous computation callback
|
|
24
|
+
* @returns {Computed<T>} - Computed signal
|
|
25
|
+
*/
|
|
26
|
+
const memo = <T extends {}>(fn: MemoCallback<T>): Computed<T> => {
|
|
27
|
+
const watchers: Set<Watcher> = new Set()
|
|
28
|
+
|
|
29
|
+
// Internal state - simplified for sync only
|
|
30
|
+
let value: T = UNSET
|
|
31
|
+
let error: Error | undefined
|
|
32
|
+
let dirty = true
|
|
33
|
+
let computing = false
|
|
34
|
+
|
|
35
|
+
// Called when notified from sources (push)
|
|
36
|
+
const mark = (() => {
|
|
37
|
+
dirty = true
|
|
38
|
+
if (watchers.size) {
|
|
39
|
+
notify(watchers)
|
|
40
|
+
} else {
|
|
41
|
+
mark.cleanups.forEach(fn => fn())
|
|
42
|
+
mark.cleanups.clear()
|
|
43
|
+
}
|
|
44
|
+
}) as Watcher
|
|
45
|
+
mark.cleanups = new Set<Cleanup>()
|
|
46
|
+
|
|
47
|
+
// Called when requested by dependencies (pull)
|
|
48
|
+
const compute = () =>
|
|
49
|
+
watch(() => {
|
|
50
|
+
if (computing) throw new CircularDependencyError('memo')
|
|
51
|
+
computing = true
|
|
52
|
+
try {
|
|
53
|
+
const result = fn()
|
|
54
|
+
if (null == result || UNSET === result) {
|
|
55
|
+
value = UNSET
|
|
56
|
+
error = undefined
|
|
57
|
+
} else {
|
|
58
|
+
value = result
|
|
59
|
+
dirty = false
|
|
60
|
+
error = undefined
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
value = UNSET
|
|
64
|
+
error = e instanceof Error ? e : new Error(String(e))
|
|
65
|
+
} finally {
|
|
66
|
+
computing = false
|
|
67
|
+
}
|
|
68
|
+
}, mark)
|
|
69
|
+
|
|
70
|
+
const c: Computed<T> = {
|
|
71
|
+
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the current value of the computed
|
|
75
|
+
*
|
|
76
|
+
* @returns {T} - current value of the computed
|
|
77
|
+
*/
|
|
78
|
+
get: (): T => {
|
|
79
|
+
subscribe(watchers)
|
|
80
|
+
flush()
|
|
81
|
+
if (dirty) compute()
|
|
82
|
+
if (error) throw error
|
|
83
|
+
return value
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
return c
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* === Exports === */
|
|
90
|
+
|
|
91
|
+
export { type MemoCallback, memo }
|
package/src/scheduler.d.ts
CHANGED
|
@@ -1,42 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
type Cleanup = () => void;
|
|
2
|
+
type Watcher = {
|
|
3
3
|
(): void;
|
|
4
|
-
cleanups: Set<
|
|
4
|
+
cleanups: Set<Cleanup>;
|
|
5
5
|
};
|
|
6
|
-
|
|
6
|
+
type Updater = <T>() => T | boolean | void;
|
|
7
7
|
/**
|
|
8
8
|
* Add active watcher to the Set of watchers
|
|
9
9
|
*
|
|
10
10
|
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
declare const subscribe: (watchers: Set<Watcher>) => void;
|
|
13
13
|
/**
|
|
14
14
|
* Add watchers to the pending set of change notifications
|
|
15
15
|
*
|
|
16
16
|
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
declare const notify: (watchers: Set<Watcher>) => void;
|
|
19
19
|
/**
|
|
20
20
|
* Flush all pending changes to notify watchers
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
declare const flush: () => void;
|
|
23
23
|
/**
|
|
24
24
|
* Batch multiple changes in a single signal graph and DOM update cycle
|
|
25
25
|
*
|
|
26
26
|
* @param {() => void} fn - function with multiple signal writes to be batched
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
declare const batch: (fn: () => void) => void;
|
|
29
29
|
/**
|
|
30
30
|
* Run a function in a reactive context
|
|
31
31
|
*
|
|
32
32
|
* @param {() => void} run - function to run the computation or effect
|
|
33
33
|
* @param {Watcher} mark - 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)
|
|
34
34
|
*/
|
|
35
|
-
|
|
35
|
+
declare const watch: (run: () => void, mark?: Watcher) => void;
|
|
36
36
|
/**
|
|
37
37
|
* Enqueue a function to be executed on the next animation frame
|
|
38
38
|
*
|
|
39
|
+
* If the same Symbol is provided for multiple calls before the next animation frame,
|
|
40
|
+
* only the latest call will be executed (deduplication).
|
|
41
|
+
*
|
|
39
42
|
* @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
|
|
40
|
-
* @param {
|
|
43
|
+
* @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
|
|
41
44
|
*/
|
|
42
|
-
|
|
45
|
+
declare const enqueue: <T>(fn: Updater, dedupe?: symbol) => Promise<boolean | void | T>;
|
|
46
|
+
export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, enqueue, };
|
package/src/scheduler.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* === Types === */
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type Cleanup = () => void
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type Watcher = {
|
|
6
6
|
(): void
|
|
7
|
-
cleanups: Set<
|
|
7
|
+
cleanups: Set<Cleanup>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type Updater = <T>() => T | boolean | void
|
|
11
11
|
|
|
12
12
|
/* === Internal === */
|
|
13
13
|
|
|
@@ -18,8 +18,8 @@ let active: Watcher | undefined
|
|
|
18
18
|
const pending = new Set<Watcher>()
|
|
19
19
|
let batchDepth = 0
|
|
20
20
|
|
|
21
|
-
// Map of
|
|
22
|
-
const updateMap = new Map<
|
|
21
|
+
// Map of deduplication symbols to update functions (using Symbol keys prevents unintended overwrites)
|
|
22
|
+
const updateMap = new Map<symbol, Updater>()
|
|
23
23
|
let requestId: number | undefined
|
|
24
24
|
|
|
25
25
|
const updateDOM = () => {
|
|
@@ -39,14 +39,14 @@ const requestTick = () => {
|
|
|
39
39
|
// Initial render when the call stack is empty
|
|
40
40
|
queueMicrotask(updateDOM)
|
|
41
41
|
|
|
42
|
-
/* ===
|
|
42
|
+
/* === Functions === */
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Add active watcher to the Set of watchers
|
|
46
46
|
*
|
|
47
47
|
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
const subscribe = (watchers: Set<Watcher>) => {
|
|
50
50
|
// if (!active) console.warn('Calling .get() outside of a reactive context')
|
|
51
51
|
if (active && !watchers.has(active)) {
|
|
52
52
|
const watcher = active
|
|
@@ -62,7 +62,7 @@ export const subscribe = (watchers: Set<Watcher>) => {
|
|
|
62
62
|
*
|
|
63
63
|
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
const notify = (watchers: Set<Watcher>) => {
|
|
66
66
|
for (const mark of watchers) {
|
|
67
67
|
if (batchDepth) pending.add(mark)
|
|
68
68
|
else mark()
|
|
@@ -72,7 +72,7 @@ export const notify = (watchers: Set<Watcher>) => {
|
|
|
72
72
|
/**
|
|
73
73
|
* Flush all pending changes to notify watchers
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
const flush = () => {
|
|
76
76
|
while (pending.size) {
|
|
77
77
|
const watchers = Array.from(pending)
|
|
78
78
|
pending.clear()
|
|
@@ -87,7 +87,7 @@ export const flush = () => {
|
|
|
87
87
|
*
|
|
88
88
|
* @param {() => void} fn - function with multiple signal writes to be batched
|
|
89
89
|
*/
|
|
90
|
-
|
|
90
|
+
const batch = (fn: () => void) => {
|
|
91
91
|
batchDepth++
|
|
92
92
|
try {
|
|
93
93
|
fn()
|
|
@@ -103,7 +103,7 @@ export const batch = (fn: () => void) => {
|
|
|
103
103
|
* @param {() => void} run - function to run the computation or effect
|
|
104
104
|
* @param {Watcher} mark - 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)
|
|
105
105
|
*/
|
|
106
|
-
|
|
106
|
+
const watch = (run: () => void, mark?: Watcher): void => {
|
|
107
107
|
const prev = active
|
|
108
108
|
active = mark
|
|
109
109
|
try {
|
|
@@ -116,12 +116,15 @@ export const watch = (run: () => void, mark?: Watcher): void => {
|
|
|
116
116
|
/**
|
|
117
117
|
* Enqueue a function to be executed on the next animation frame
|
|
118
118
|
*
|
|
119
|
+
* If the same Symbol is provided for multiple calls before the next animation frame,
|
|
120
|
+
* only the latest call will be executed (deduplication).
|
|
121
|
+
*
|
|
119
122
|
* @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
|
|
120
|
-
* @param {
|
|
123
|
+
* @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
|
|
121
124
|
*/
|
|
122
|
-
|
|
125
|
+
const enqueue = <T>(fn: Updater, dedupe?: symbol) =>
|
|
123
126
|
new Promise<T | boolean | void>((resolve, reject) => {
|
|
124
|
-
updateMap.set(dedupe, () => {
|
|
127
|
+
updateMap.set(dedupe || Symbol(), () => {
|
|
125
128
|
try {
|
|
126
129
|
resolve(fn())
|
|
127
130
|
} catch (error) {
|
|
@@ -130,3 +133,17 @@ export const enqueue = <T>(fn: Updater, dedupe: EnqueueDedupe) =>
|
|
|
130
133
|
})
|
|
131
134
|
requestTick()
|
|
132
135
|
})
|
|
136
|
+
|
|
137
|
+
/* === Exports === */
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
type Cleanup,
|
|
141
|
+
type Watcher,
|
|
142
|
+
type Updater,
|
|
143
|
+
subscribe,
|
|
144
|
+
notify,
|
|
145
|
+
flush,
|
|
146
|
+
batch,
|
|
147
|
+
watch,
|
|
148
|
+
enqueue,
|
|
149
|
+
}
|