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