@zeix/cause-effect 0.14.0 → 0.14.2
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 +68 -72
- package/biome.json +35 -0
- package/index.d.ts +6 -8
- package/index.dev.js +294 -0
- package/index.js +1 -1
- package/index.ts +26 -14
- package/package.json +5 -6
- package/src/computed.d.ts +11 -9
- package/src/computed.ts +138 -14
- package/src/effect.d.ts +7 -9
- package/src/effect.ts +43 -58
- package/src/scheduler.d.ts +15 -6
- package/src/scheduler.ts +39 -16
- package/src/signal.d.ts +5 -10
- package/src/signal.ts +10 -15
- package/src/state.d.ts +2 -1
- package/src/state.ts +1 -1
- package/src/util.d.ts +1 -5
- package/src/util.ts +3 -23
- package/test/batch.test.ts +13 -13
- package/test/benchmark.test.ts +79 -43
- package/test/computed.test.ts +76 -78
- package/test/effect.test.ts +24 -24
- package/test/state.test.ts +33 -33
- 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/src/memo.d.ts +0 -13
- package/src/memo.ts +0 -91
- package/src/task.d.ts +0 -17
- package/src/task.ts +0 -153
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeix/cause-effect",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"author": "Esther Brunner",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"devDependencies": {
|
|
8
|
+
"@biomejs/biome": "2.1.4",
|
|
8
9
|
"@types/bun": "latest",
|
|
9
|
-
"
|
|
10
|
-
"random": "^5.4.0",
|
|
11
|
-
"typescript-eslint": "^8.32.1"
|
|
10
|
+
"random": "^5.4.1"
|
|
12
11
|
},
|
|
13
12
|
"peerDependencies": {
|
|
14
13
|
"typescript": "^5.6.3"
|
|
@@ -25,9 +24,9 @@
|
|
|
25
24
|
"access": "public"
|
|
26
25
|
},
|
|
27
26
|
"scripts": {
|
|
28
|
-
"build": "bun build index.ts --outdir ./ --minify &&
|
|
27
|
+
"build": "bunx tsc && bun build index.ts --outdir ./ --minify && bun build index.ts --outfile index.dev.js",
|
|
29
28
|
"test": "bun test",
|
|
30
|
-
"lint": "bunx
|
|
29
|
+
"lint": "bunx biome lint --write"
|
|
31
30
|
},
|
|
32
31
|
"type": "module",
|
|
33
32
|
"types": "index.d.ts"
|
package/src/computed.d.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import { type MemoCallback } from './memo';
|
|
2
|
-
import { type TaskCallback } from './task';
|
|
3
1
|
type Computed<T extends {}> = {
|
|
4
2
|
[Symbol.toStringTag]: 'Computed';
|
|
5
3
|
get(): T;
|
|
6
4
|
};
|
|
7
5
|
type ComputedCallback<T extends {} & {
|
|
8
|
-
then?:
|
|
9
|
-
}> =
|
|
6
|
+
then?: undefined;
|
|
7
|
+
}> = ((abort: AbortSignal) => Promise<T>) | (() => T);
|
|
10
8
|
declare const TYPE_COMPUTED = "Computed";
|
|
11
9
|
/**
|
|
12
10
|
* Create a derived signal from existing signals
|
|
13
11
|
*
|
|
14
|
-
* This function delegates to either memo() for synchronous computations
|
|
15
|
-
* or task() for asynchronous computations, providing better performance
|
|
16
|
-
* for each case.
|
|
17
|
-
*
|
|
18
12
|
* @since 0.9.0
|
|
19
13
|
* @param {ComputedCallback<T>} fn - computation callback function
|
|
20
14
|
* @returns {Computed<T>} - Computed signal
|
|
@@ -28,4 +22,12 @@ declare const computed: <T extends {}>(fn: ComputedCallback<T>) => Computed<T>;
|
|
|
28
22
|
* @returns {boolean} - true if value is a computed state, false otherwise
|
|
29
23
|
*/
|
|
30
24
|
declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
|
|
31
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
27
|
+
*
|
|
28
|
+
* @since 0.12.0
|
|
29
|
+
* @param {unknown} value - value to check
|
|
30
|
+
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
declare const isComputedCallback: <T extends {}>(value: unknown) => value is ComputedCallback<T>;
|
|
33
|
+
export { TYPE_COMPUTED, computed, isComputed, isComputedCallback, type Computed, type ComputedCallback, };
|
package/src/computed.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
flush,
|
|
3
|
+
notify,
|
|
4
|
+
observe,
|
|
5
|
+
subscribe,
|
|
6
|
+
type Watcher,
|
|
7
|
+
watch,
|
|
8
|
+
} from './scheduler'
|
|
9
|
+
import { UNSET } from './signal'
|
|
10
|
+
import {
|
|
11
|
+
CircularDependencyError,
|
|
12
|
+
isFunction,
|
|
13
|
+
isObjectOfType,
|
|
14
|
+
toError,
|
|
15
|
+
} from './util'
|
|
4
16
|
|
|
5
17
|
/* === Types === */
|
|
6
18
|
|
|
@@ -8,9 +20,9 @@ type Computed<T extends {}> = {
|
|
|
8
20
|
[Symbol.toStringTag]: 'Computed'
|
|
9
21
|
get(): T
|
|
10
22
|
}
|
|
11
|
-
type ComputedCallback<T extends {} & { then?:
|
|
12
|
-
|
|
|
13
|
-
|
|
|
23
|
+
type ComputedCallback<T extends {} & { then?: undefined }> =
|
|
24
|
+
| ((abort: AbortSignal) => Promise<T>)
|
|
25
|
+
| (() => T)
|
|
14
26
|
|
|
15
27
|
/* === Constants === */
|
|
16
28
|
|
|
@@ -21,16 +33,116 @@ const TYPE_COMPUTED = 'Computed'
|
|
|
21
33
|
/**
|
|
22
34
|
* Create a derived signal from existing signals
|
|
23
35
|
*
|
|
24
|
-
* This function delegates to either memo() for synchronous computations
|
|
25
|
-
* or task() for asynchronous computations, providing better performance
|
|
26
|
-
* for each case.
|
|
27
|
-
*
|
|
28
36
|
* @since 0.9.0
|
|
29
37
|
* @param {ComputedCallback<T>} fn - computation callback function
|
|
30
38
|
* @returns {Computed<T>} - Computed signal
|
|
31
39
|
*/
|
|
32
|
-
const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> =>
|
|
33
|
-
|
|
40
|
+
const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
41
|
+
const watchers: Set<Watcher> = new Set()
|
|
42
|
+
|
|
43
|
+
// Internal state
|
|
44
|
+
let value: T = UNSET
|
|
45
|
+
let error: Error | undefined
|
|
46
|
+
let controller: AbortController | undefined
|
|
47
|
+
let dirty = true
|
|
48
|
+
let changed = false
|
|
49
|
+
let computing = false
|
|
50
|
+
|
|
51
|
+
// Functions to update internal state
|
|
52
|
+
const ok = (v: T): undefined => {
|
|
53
|
+
if (!Object.is(v, value)) {
|
|
54
|
+
value = v
|
|
55
|
+
changed = true
|
|
56
|
+
}
|
|
57
|
+
error = undefined
|
|
58
|
+
dirty = false
|
|
59
|
+
}
|
|
60
|
+
const nil = (): undefined => {
|
|
61
|
+
changed = UNSET !== value
|
|
62
|
+
value = UNSET
|
|
63
|
+
error = undefined
|
|
64
|
+
}
|
|
65
|
+
const err = (e: unknown): undefined => {
|
|
66
|
+
const newError = toError(e)
|
|
67
|
+
changed =
|
|
68
|
+
!error ||
|
|
69
|
+
newError.name !== error.name ||
|
|
70
|
+
newError.message !== error.message
|
|
71
|
+
value = UNSET
|
|
72
|
+
error = newError
|
|
73
|
+
}
|
|
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
|
+
}
|
|
82
|
+
|
|
83
|
+
// Own watcher: called when notified from sources (push)
|
|
84
|
+
const mark = watch(() => {
|
|
85
|
+
dirty = true
|
|
86
|
+
controller?.abort('Aborted because source signal changed')
|
|
87
|
+
if (watchers.size) notify(watchers)
|
|
88
|
+
else mark.cleanup()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Called when requested by dependencies (pull)
|
|
92
|
+
const compute = () =>
|
|
93
|
+
observe(() => {
|
|
94
|
+
if (computing) throw new CircularDependencyError('computed')
|
|
95
|
+
changed = false
|
|
96
|
+
if (isFunction(fn) && fn.constructor.name === 'AsyncFunction') {
|
|
97
|
+
if (controller) return value // return current value until promise resolves
|
|
98
|
+
controller = new AbortController()
|
|
99
|
+
controller.signal.addEventListener(
|
|
100
|
+
'abort',
|
|
101
|
+
() => {
|
|
102
|
+
computing = false
|
|
103
|
+
controller = undefined
|
|
104
|
+
compute() // retry
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
once: true,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
let result: T | Promise<T>
|
|
112
|
+
computing = true
|
|
113
|
+
try {
|
|
114
|
+
result = controller ? fn(controller.signal) : (fn as () => T)()
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (e instanceof DOMException && e.name === 'AbortError') nil()
|
|
117
|
+
else err(e)
|
|
118
|
+
computing = false
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (result instanceof Promise) result.then(settle(ok), settle(err))
|
|
122
|
+
else if (null == result || UNSET === result) nil()
|
|
123
|
+
else ok(result)
|
|
124
|
+
computing = false
|
|
125
|
+
}, mark)
|
|
126
|
+
|
|
127
|
+
const c: Computed<T> = {
|
|
128
|
+
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the current value of the computed
|
|
132
|
+
*
|
|
133
|
+
* @since 0.9.0
|
|
134
|
+
* @returns {T} - current value of the computed
|
|
135
|
+
*/
|
|
136
|
+
get: (): T => {
|
|
137
|
+
subscribe(watchers)
|
|
138
|
+
flush()
|
|
139
|
+
if (dirty) compute()
|
|
140
|
+
if (error) throw error
|
|
141
|
+
return value
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
return c
|
|
145
|
+
}
|
|
34
146
|
|
|
35
147
|
/**
|
|
36
148
|
* Check if a value is a computed state
|
|
@@ -43,12 +155,24 @@ const isComputed = /*#__PURE__*/ <T extends {}>(
|
|
|
43
155
|
value: unknown,
|
|
44
156
|
): value is Computed<T> => isObjectOfType(value, TYPE_COMPUTED)
|
|
45
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
|
+
|
|
46
169
|
/* === Exports === */
|
|
47
170
|
|
|
48
171
|
export {
|
|
49
|
-
type Computed,
|
|
50
|
-
type ComputedCallback,
|
|
51
172
|
TYPE_COMPUTED,
|
|
52
173
|
computed,
|
|
53
174
|
isComputed,
|
|
175
|
+
isComputedCallback,
|
|
176
|
+
type Computed,
|
|
177
|
+
type ComputedCallback,
|
|
54
178
|
}
|
package/src/effect.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { type Signal } from './signal';
|
|
2
1
|
import { type Cleanup } from './scheduler';
|
|
3
|
-
type
|
|
2
|
+
import { type Signal, type SignalValues } from './signal';
|
|
3
|
+
type EffectMatcher<S extends Signal<unknown & {}>[]> = {
|
|
4
4
|
signals: S;
|
|
5
|
-
ok: (...values:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
err?: (...errors: Error[]) => void | Cleanup;
|
|
9
|
-
nil?: () => void | Cleanup;
|
|
5
|
+
ok: (...values: SignalValues<S>) => Cleanup | undefined;
|
|
6
|
+
err?: (...errors: Error[]) => Cleanup | undefined;
|
|
7
|
+
nil?: () => Cleanup | undefined;
|
|
10
8
|
};
|
|
11
9
|
/**
|
|
12
10
|
* Define what happens when a reactive state changes
|
|
13
11
|
*
|
|
14
12
|
* @since 0.1.0
|
|
15
|
-
* @param {EffectMatcher<S> | (() =>
|
|
13
|
+
* @param {EffectMatcher<S> | (() => Cleanup | undefined)} matcher - effect matcher or callback
|
|
16
14
|
* @returns {Cleanup} - cleanup function for the effect
|
|
17
15
|
*/
|
|
18
|
-
declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() =>
|
|
16
|
+
declare function effect<S extends Signal<unknown & {}>[]>(matcher: EffectMatcher<S> | (() => Cleanup | undefined)): Cleanup;
|
|
19
17
|
export { type EffectMatcher, effect };
|
package/src/effect.ts
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
isFunction,
|
|
5
|
-
toError,
|
|
6
|
-
isAbortError,
|
|
7
|
-
} from './util'
|
|
8
|
-
import { watch, type Cleanup, type Watcher } from './scheduler'
|
|
1
|
+
import { type Cleanup, observe, watch } from './scheduler'
|
|
2
|
+
import { type Signal, type SignalValues, UNSET } from './signal'
|
|
3
|
+
import { CircularDependencyError, isFunction, toError } from './util'
|
|
9
4
|
|
|
10
5
|
/* === Types === */
|
|
11
6
|
|
|
12
|
-
type EffectMatcher<S extends Signal<{}>[]> = {
|
|
7
|
+
type EffectMatcher<S extends Signal<unknown & {}>[]> = {
|
|
13
8
|
signals: S
|
|
14
|
-
ok: (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
) => void | Cleanup
|
|
19
|
-
err?: (...errors: Error[]) => void | Cleanup
|
|
20
|
-
nil?: () => void | Cleanup
|
|
9
|
+
ok: (...values: SignalValues<S>) => Cleanup | undefined
|
|
10
|
+
err?: (...errors: Error[]) => Cleanup | undefined
|
|
11
|
+
nil?: () => Cleanup | undefined
|
|
21
12
|
}
|
|
22
13
|
|
|
23
14
|
/* === Functions === */
|
|
@@ -26,68 +17,62 @@ type EffectMatcher<S extends Signal<{}>[]> = {
|
|
|
26
17
|
* Define what happens when a reactive state changes
|
|
27
18
|
*
|
|
28
19
|
* @since 0.1.0
|
|
29
|
-
* @param {EffectMatcher<S> | (() =>
|
|
20
|
+
* @param {EffectMatcher<S> | (() => Cleanup | undefined)} matcher - effect matcher or callback
|
|
30
21
|
* @returns {Cleanup} - cleanup function for the effect
|
|
31
22
|
*/
|
|
32
|
-
function effect<S extends Signal<{}>[]>(
|
|
33
|
-
matcher: EffectMatcher<S> | (() =>
|
|
23
|
+
function effect<S extends Signal<unknown & {}>[]>(
|
|
24
|
+
matcher: EffectMatcher<S> | (() => Cleanup | undefined),
|
|
34
25
|
): Cleanup {
|
|
35
26
|
const {
|
|
36
27
|
signals,
|
|
37
28
|
ok,
|
|
38
|
-
err =
|
|
39
|
-
|
|
29
|
+
err = (error: Error): undefined => {
|
|
30
|
+
console.error(error)
|
|
31
|
+
},
|
|
32
|
+
nil = (): undefined => {},
|
|
40
33
|
} = isFunction(matcher)
|
|
41
34
|
? { signals: [] as unknown as S, ok: matcher }
|
|
42
35
|
: matcher
|
|
36
|
+
|
|
43
37
|
let running = false
|
|
44
|
-
const run = (() =>
|
|
45
|
-
|
|
38
|
+
const run = watch(() =>
|
|
39
|
+
observe(() => {
|
|
46
40
|
if (running) throw new CircularDependencyError('effect')
|
|
47
41
|
running = true
|
|
48
|
-
let cleanup: void | Cleanup = undefined
|
|
49
|
-
try {
|
|
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
42
|
|
|
43
|
+
// Pure part
|
|
44
|
+
const errors: Error[] = []
|
|
45
|
+
let pending = false
|
|
46
|
+
const values = signals.map(signal => {
|
|
68
47
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
? err(...errors)
|
|
73
|
-
: ok(...values)
|
|
48
|
+
const value = signal.get()
|
|
49
|
+
if (value === UNSET) pending = true
|
|
50
|
+
return value
|
|
74
51
|
} catch (e) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
cleanup = err(error)
|
|
52
|
+
errors.push(toError(e))
|
|
53
|
+
return UNSET
|
|
78
54
|
}
|
|
55
|
+
}) as SignalValues<S>
|
|
56
|
+
|
|
57
|
+
// Effectful part
|
|
58
|
+
let cleanup: Cleanup | undefined
|
|
59
|
+
try {
|
|
60
|
+
cleanup = pending
|
|
61
|
+
? nil()
|
|
62
|
+
: errors.length
|
|
63
|
+
? err(...errors)
|
|
64
|
+
: ok(...values)
|
|
79
65
|
} catch (e) {
|
|
80
|
-
err(toError(e))
|
|
66
|
+
cleanup = err(toError(e))
|
|
67
|
+
} finally {
|
|
68
|
+
if (isFunction(cleanup)) run.off(cleanup)
|
|
81
69
|
}
|
|
82
|
-
|
|
70
|
+
|
|
83
71
|
running = false
|
|
84
|
-
}, run)
|
|
85
|
-
|
|
72
|
+
}, run),
|
|
73
|
+
)
|
|
86
74
|
run()
|
|
87
|
-
return () =>
|
|
88
|
-
run.cleanups.forEach(fn => fn())
|
|
89
|
-
run.cleanups.clear()
|
|
90
|
-
}
|
|
75
|
+
return () => run.cleanup()
|
|
91
76
|
}
|
|
92
77
|
|
|
93
78
|
/* === Exports === */
|
package/src/scheduler.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
type Cleanup = () => void;
|
|
2
2
|
type Watcher = {
|
|
3
3
|
(): void;
|
|
4
|
-
|
|
4
|
+
off(cleanup: Cleanup): void;
|
|
5
|
+
cleanup(): void;
|
|
5
6
|
};
|
|
6
|
-
type Updater = <T>() => T | boolean |
|
|
7
|
+
type Updater = <T>() => T | boolean | undefined;
|
|
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
|
*
|
|
@@ -30,9 +39,9 @@ declare const batch: (fn: () => void) => void;
|
|
|
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
|
-
declare const
|
|
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
|
*
|
|
@@ -42,5 +51,5 @@ declare const watch: (run: () => void, mark?: Watcher) => void;
|
|
|
42
51
|
* @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
|
|
43
52
|
* @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
|
|
44
53
|
*/
|
|
45
|
-
declare const enqueue: <T>(fn: Updater, dedupe?: symbol) => Promise<boolean |
|
|
46
|
-
export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, enqueue, };
|
|
54
|
+
declare const enqueue: <T>(fn: Updater, dedupe?: symbol) => Promise<boolean | T | undefined>;
|
|
55
|
+
export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, observe, enqueue, };
|
package/src/scheduler.ts
CHANGED
|
@@ -4,10 +4,11 @@ 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
|
-
type Updater = <T>() => T | boolean |
|
|
11
|
+
type Updater = <T>() => T | boolean | undefined
|
|
11
12
|
|
|
12
13
|
/* === Internal === */
|
|
13
14
|
|
|
@@ -26,8 +27,8 @@ 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
|
|
|
@@ -41,17 +42,38 @@ queueMicrotask(updateDOM)
|
|
|
41
42
|
|
|
42
43
|
/* === Functions === */
|
|
43
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
|
+
}
|
|
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
72
|
const subscribe = (watchers: Set<Watcher>) => {
|
|
50
|
-
// if (!active) console.warn('Calling .get() outside of a reactive context')
|
|
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
|
}
|
|
@@ -63,9 +85,9 @@ const subscribe = (watchers: Set<Watcher>) => {
|
|
|
63
85
|
* @param {Set<Watcher>} watchers - watchers of the signal
|
|
64
86
|
*/
|
|
65
87
|
const notify = (watchers: Set<Watcher>) => {
|
|
66
|
-
for (const
|
|
67
|
-
if (batchDepth) pending.add(
|
|
68
|
-
else
|
|
88
|
+
for (const watcher of watchers) {
|
|
89
|
+
if (batchDepth) pending.add(watcher)
|
|
90
|
+
else watcher()
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
|
|
@@ -76,8 +98,8 @@ 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
|
}
|
|
@@ -101,11 +123,11 @@ 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
|
-
const
|
|
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 {
|
|
@@ -123,8 +145,8 @@ const watch = (run: () => void, mark?: Watcher): void => {
|
|
|
123
145
|
* @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
|
|
124
146
|
*/
|
|
125
147
|
const enqueue = <T>(fn: Updater, dedupe?: symbol) =>
|
|
126
|
-
new Promise<T | boolean |
|
|
127
|
-
updateMap.set(dedupe || Symbol(), () => {
|
|
148
|
+
new Promise<T | boolean | undefined>((resolve, reject) => {
|
|
149
|
+
updateMap.set(dedupe || Symbol(), (): undefined => {
|
|
128
150
|
try {
|
|
129
151
|
resolve(fn())
|
|
130
152
|
} catch (error) {
|
|
@@ -145,5 +167,6 @@ export {
|
|
|
145
167
|
flush,
|
|
146
168
|
batch,
|
|
147
169
|
watch,
|
|
170
|
+
observe,
|
|
148
171
|
enqueue,
|
|
149
172
|
}
|
package/src/signal.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { type ComputedCallback } from './computed';
|
|
1
|
+
import { type ComputedCallback, isComputedCallback } from './computed';
|
|
2
2
|
type Signal<T extends {}> = {
|
|
3
3
|
get(): T;
|
|
4
4
|
};
|
|
5
5
|
type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>;
|
|
6
|
+
type SignalValues<S extends Signal<unknown & {}>[]> = {
|
|
7
|
+
[K in keyof S]: S[K] extends Signal<infer T> ? T : never;
|
|
8
|
+
};
|
|
6
9
|
declare const UNSET: any;
|
|
7
10
|
/**
|
|
8
11
|
* Check whether a value is a Signal or not
|
|
@@ -12,14 +15,6 @@ declare const UNSET: any;
|
|
|
12
15
|
* @returns {boolean} - true if value is a Signal, false otherwise
|
|
13
16
|
*/
|
|
14
17
|
declare const isSignal: <T extends {}>(value: unknown) => value is Signal<T>;
|
|
15
|
-
/**
|
|
16
|
-
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
17
|
-
*
|
|
18
|
-
* @since 0.12.0
|
|
19
|
-
* @param {unknown} value - value to check
|
|
20
|
-
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
21
|
-
*/
|
|
22
|
-
declare const isComputedCallback: <T extends {}>(value: unknown) => value is ComputedCallback<T>;
|
|
23
18
|
/**
|
|
24
19
|
* Convert a value to a Signal if it's not already a Signal
|
|
25
20
|
*
|
|
@@ -28,4 +23,4 @@ declare const isComputedCallback: <T extends {}>(value: unknown) => value is Com
|
|
|
28
23
|
* @returns {Signal<T>} - converted Signal
|
|
29
24
|
*/
|
|
30
25
|
declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
|
|
31
|
-
export { type Signal, type MaybeSignal, UNSET, isSignal, isComputedCallback, toSignal, };
|
|
26
|
+
export { type Signal, type MaybeSignal, type SignalValues, UNSET, isSignal, isComputedCallback, toSignal, };
|
package/src/signal.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { isState, state } from './state'
|
|
2
1
|
import {
|
|
3
2
|
type ComputedCallback,
|
|
4
|
-
isComputed,
|
|
5
3
|
computed,
|
|
4
|
+
isComputed,
|
|
5
|
+
isComputedCallback,
|
|
6
6
|
} from './computed'
|
|
7
|
-
import {
|
|
7
|
+
import { isState, state } from './state'
|
|
8
8
|
|
|
9
9
|
/* === Types === */
|
|
10
10
|
|
|
@@ -13,11 +13,16 @@ type Signal<T extends {}> = {
|
|
|
13
13
|
}
|
|
14
14
|
type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
|
|
15
15
|
|
|
16
|
+
type SignalValues<S extends Signal<unknown & {}>[]> = {
|
|
17
|
+
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
/* === Constants === */
|
|
17
21
|
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: Deliberately using any to be used as a placeholder value in any signal
|
|
18
23
|
const UNSET: any = Symbol()
|
|
19
24
|
|
|
20
|
-
/* ===
|
|
25
|
+
/* === Functions === */
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Check whether a value is a Signal or not
|
|
@@ -30,17 +35,6 @@ const isSignal = /*#__PURE__*/ <T extends {}>(
|
|
|
30
35
|
value: unknown,
|
|
31
36
|
): value is Signal<T> => isState(value) || isComputed(value)
|
|
32
37
|
|
|
33
|
-
/**
|
|
34
|
-
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
35
|
-
*
|
|
36
|
-
* @since 0.12.0
|
|
37
|
-
* @param {unknown} value - value to check
|
|
38
|
-
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
39
|
-
*/
|
|
40
|
-
const isComputedCallback = /*#__PURE__*/ <T extends {}>(
|
|
41
|
-
value: unknown,
|
|
42
|
-
): value is ComputedCallback<T> => isFunction(value) && value.length < 2
|
|
43
|
-
|
|
44
38
|
/**
|
|
45
39
|
* Convert a value to a Signal if it's not already a Signal
|
|
46
40
|
*
|
|
@@ -62,6 +56,7 @@ const toSignal = /*#__PURE__*/ <T extends {}>(
|
|
|
62
56
|
export {
|
|
63
57
|
type Signal,
|
|
64
58
|
type MaybeSignal,
|
|
59
|
+
type SignalValues,
|
|
65
60
|
UNSET,
|
|
66
61
|
isSignal,
|
|
67
62
|
isComputedCallback,
|
package/src/state.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ type State<T extends {}> = {
|
|
|
4
4
|
set(v: T): void;
|
|
5
5
|
update(fn: (v: T) => T): void;
|
|
6
6
|
};
|
|
7
|
+
declare const TYPE_STATE = "State";
|
|
7
8
|
/**
|
|
8
9
|
* Create a new state signal
|
|
9
10
|
*
|
|
@@ -20,4 +21,4 @@ declare const state: <T extends {}>(initialValue: T) => State<T>;
|
|
|
20
21
|
* @returns {boolean} - true if the value is a State instance, false otherwise
|
|
21
22
|
*/
|
|
22
23
|
declare const isState: <T extends {}>(value: unknown) => value is State<T>;
|
|
23
|
-
export { type State, state, isState };
|
|
24
|
+
export { type State, TYPE_STATE, state, isState };
|
package/src/state.ts
CHANGED