@zeix/cause-effect 0.13.1 → 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 +161 -131
- package/eslint.config.js +35 -0
- package/index.d.ts +9 -7
- package/index.js +1 -1
- package/index.ts +20 -10
- package/package.json +32 -29
- package/src/computed.d.ts +31 -0
- package/src/computed.ts +54 -0
- package/src/effect.d.ts +19 -0
- package/src/effect.ts +95 -0
- package/src/memo.d.ts +13 -0
- package/src/memo.ts +91 -0
- package/{lib → src}/scheduler.d.ts +15 -11
- package/{lib → src}/scheduler.ts +60 -48
- package/src/signal.d.ts +31 -0
- package/src/signal.ts +69 -0
- package/{lib → src}/state.d.ts +4 -7
- package/src/state.ts +89 -0
- package/src/task.d.ts +17 -0
- package/src/task.ts +153 -0
- package/{lib → src}/util.d.ts +1 -1
- package/{lib → src}/util.ts +23 -11
- package/test/batch.test.ts +23 -28
- package/test/benchmark.test.ts +115 -103
- package/test/computed.test.ts +133 -147
- package/test/effect.test.ts +42 -37
- package/test/state.test.ts +12 -79
- package/test/util/dependency-graph.ts +147 -145
- package/test/util/framework-types.ts +22 -22
- package/test/util/perf-tests.ts +28 -28
- package/test/util/reactive-framework.ts +11 -12
- package/lib/computed.d.ts +0 -33
- package/lib/computed.ts +0 -206
- package/lib/effect.d.ts +0 -22
- package/lib/effect.ts +0 -61
- package/lib/signal.d.ts +0 -45
- package/lib/signal.ts +0 -102
- package/lib/state.ts +0 -118
package/package.json
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
2
|
+
"name": "@zeix/cause-effect",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"author": "Esther Brunner",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
"eslint": "^9.27.0",
|
|
10
|
+
"random": "^5.4.0",
|
|
11
|
+
"typescript-eslint": "^8.32.1"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"typescript": "^5.6.3"
|
|
15
|
+
},
|
|
16
|
+
"description": "Cause & Effect - reactive state management with signals.",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"Cause & Effect",
|
|
20
|
+
"Reactivity",
|
|
21
|
+
"Signals",
|
|
22
|
+
"Effects"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "bun build index.ts --outdir ./ --minify && bunx tsc",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"lint": "bunx eslint src/"
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"types": "index.d.ts"
|
|
31
34
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type MemoCallback } from './memo';
|
|
2
|
+
import { type TaskCallback } from './task';
|
|
3
|
+
type Computed<T extends {}> = {
|
|
4
|
+
[Symbol.toStringTag]: 'Computed';
|
|
5
|
+
get(): T;
|
|
6
|
+
};
|
|
7
|
+
type ComputedCallback<T extends {} & {
|
|
8
|
+
then?: void;
|
|
9
|
+
}> = TaskCallback<T> | MemoCallback<T>;
|
|
10
|
+
declare const TYPE_COMPUTED = "Computed";
|
|
11
|
+
/**
|
|
12
|
+
* Create a derived signal from existing signals
|
|
13
|
+
*
|
|
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
|
+
* @since 0.9.0
|
|
19
|
+
* @param {ComputedCallback<T>} fn - computation callback function
|
|
20
|
+
* @returns {Computed<T>} - Computed signal
|
|
21
|
+
*/
|
|
22
|
+
declare const computed: <T extends {}>(fn: ComputedCallback<T>) => Computed<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if a value is a computed state
|
|
25
|
+
*
|
|
26
|
+
* @since 0.9.0
|
|
27
|
+
* @param {unknown} value - value to check
|
|
28
|
+
* @returns {boolean} - true if value is a computed state, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
declare const isComputed: <T extends {}>(value: unknown) => value is Computed<T>;
|
|
31
|
+
export { type Computed, type ComputedCallback, TYPE_COMPUTED, computed, isComputed, };
|
package/src/computed.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isAsyncFunction, isObjectOfType } from './util'
|
|
2
|
+
import { type MemoCallback, memo } from './memo'
|
|
3
|
+
import { type TaskCallback, task } from './task'
|
|
4
|
+
|
|
5
|
+
/* === Types === */
|
|
6
|
+
|
|
7
|
+
type Computed<T extends {}> = {
|
|
8
|
+
[Symbol.toStringTag]: 'Computed'
|
|
9
|
+
get(): T
|
|
10
|
+
}
|
|
11
|
+
type ComputedCallback<T extends {} & { then?: void }> =
|
|
12
|
+
| TaskCallback<T>
|
|
13
|
+
| MemoCallback<T>
|
|
14
|
+
|
|
15
|
+
/* === Constants === */
|
|
16
|
+
|
|
17
|
+
const TYPE_COMPUTED = 'Computed'
|
|
18
|
+
|
|
19
|
+
/* === Functions === */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a derived signal from existing signals
|
|
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
|
+
*
|
|
28
|
+
* @since 0.9.0
|
|
29
|
+
* @param {ComputedCallback<T>} fn - computation callback function
|
|
30
|
+
* @returns {Computed<T>} - Computed signal
|
|
31
|
+
*/
|
|
32
|
+
const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> =>
|
|
33
|
+
isAsyncFunction<T>(fn) ? task<T>(fn) : memo<T>(fn as MemoCallback<T>)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a value is a computed state
|
|
37
|
+
*
|
|
38
|
+
* @since 0.9.0
|
|
39
|
+
* @param {unknown} value - value to check
|
|
40
|
+
* @returns {boolean} - true if value is a computed state, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
const isComputed = /*#__PURE__*/ <T extends {}>(
|
|
43
|
+
value: unknown,
|
|
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
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Signal } from './signal';
|
|
2
|
+
import { type Cleanup } from './scheduler';
|
|
3
|
+
type EffectMatcher<S extends Signal<{}>[]> = {
|
|
4
|
+
signals: S;
|
|
5
|
+
ok: (...values: {
|
|
6
|
+
[K in keyof S]: S[K] extends Signal<infer T> ? T : never;
|
|
7
|
+
}) => void | Cleanup;
|
|
8
|
+
err?: (...errors: Error[]) => void | Cleanup;
|
|
9
|
+
nil?: () => void | Cleanup;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Define what happens when a reactive state changes
|
|
13
|
+
*
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
* @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
|
|
16
|
+
* @returns {Cleanup} - cleanup function for the effect
|
|
17
|
+
*/
|
|
18
|
+
declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() => void | Cleanup)): Cleanup;
|
|
19
|
+
export { type EffectMatcher, effect };
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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'
|
|
9
|
+
|
|
10
|
+
/* === Types === */
|
|
11
|
+
|
|
12
|
+
type EffectMatcher<S extends Signal<{}>[]> = {
|
|
13
|
+
signals: S
|
|
14
|
+
ok: (
|
|
15
|
+
...values: {
|
|
16
|
+
[K in keyof S]: S[K] extends Signal<infer T> ? T : never
|
|
17
|
+
}
|
|
18
|
+
) => void | Cleanup
|
|
19
|
+
err?: (...errors: Error[]) => void | Cleanup
|
|
20
|
+
nil?: () => void | Cleanup
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* === Functions === */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Define what happens when a reactive state changes
|
|
27
|
+
*
|
|
28
|
+
* @since 0.1.0
|
|
29
|
+
* @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
|
|
30
|
+
* @returns {Cleanup} - cleanup function for the effect
|
|
31
|
+
*/
|
|
32
|
+
function effect<S extends Signal<{}>[]>(
|
|
33
|
+
matcher: EffectMatcher<S> | (() => void | Cleanup),
|
|
34
|
+
): Cleanup {
|
|
35
|
+
const {
|
|
36
|
+
signals,
|
|
37
|
+
ok,
|
|
38
|
+
err = console.error,
|
|
39
|
+
nil = () => {},
|
|
40
|
+
} = isFunction(matcher)
|
|
41
|
+
? { signals: [] as unknown as S, ok: matcher }
|
|
42
|
+
: matcher
|
|
43
|
+
let running = false
|
|
44
|
+
const run = (() =>
|
|
45
|
+
watch(() => {
|
|
46
|
+
if (running) throw new CircularDependencyError('effect')
|
|
47
|
+
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
|
+
|
|
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
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
err(toError(e))
|
|
81
|
+
}
|
|
82
|
+
if (isFunction(cleanup)) run.cleanups.add(cleanup)
|
|
83
|
+
running = false
|
|
84
|
+
}, run)) as Watcher
|
|
85
|
+
run.cleanups = new Set<Cleanup>()
|
|
86
|
+
run()
|
|
87
|
+
return () => {
|
|
88
|
+
run.cleanups.forEach(fn => fn())
|
|
89
|
+
run.cleanups.clear()
|
|
90
|
+
}
|
|
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 }
|
|
@@ -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/{lib → src}/scheduler.ts
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* === Types === */
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type Cleanup = () => void
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
(): void
|
|
7
|
-
cleanups: Set<
|
|
5
|
+
type Watcher = {
|
|
6
|
+
(): void
|
|
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 = () => {
|
|
@@ -32,21 +32,21 @@ const updateDOM = () => {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const requestTick = () => {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (requestId) cancelAnimationFrame(requestId)
|
|
36
|
+
requestId = requestAnimationFrame(updateDOM)
|
|
37
37
|
}
|
|
38
38
|
|
|
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
|
|
@@ -59,51 +59,51 @@ export const subscribe = (watchers: Set<Watcher>) => {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Add watchers to the pending set of change notifications
|
|
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()
|
|
69
|
-
|
|
69
|
+
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Flush all pending changes to notify watchers
|
|
74
74
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
const flush = () => {
|
|
76
|
+
while (pending.size) {
|
|
77
|
+
const watchers = Array.from(pending)
|
|
78
|
+
pending.clear()
|
|
79
|
+
for (const mark of watchers) {
|
|
80
|
+
mark()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Batch multiple changes in a single signal graph and DOM update cycle
|
|
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()
|
|
94
94
|
} finally {
|
|
95
95
|
flush()
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
batchDepth--
|
|
97
|
+
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Run a function in a reactive context
|
|
102
|
-
*
|
|
102
|
+
*
|
|
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 {
|
|
@@ -115,23 +115,35 @@ export const watch = (run: () => void, mark?: Watcher): void => {
|
|
|
115
115
|
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
const enqueue = <T>(fn: Updater, dedupe?: symbol) =>
|
|
126
|
+
new Promise<T | boolean | void>((resolve, reject) => {
|
|
127
|
+
updateMap.set(dedupe || Symbol(), () => {
|
|
128
|
+
try {
|
|
129
|
+
resolve(fn())
|
|
130
|
+
} catch (error) {
|
|
131
|
+
reject(error)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
requestTick()
|
|
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
|
+
}
|
package/src/signal.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ComputedCallback } from './computed';
|
|
2
|
+
type Signal<T extends {}> = {
|
|
3
|
+
get(): T;
|
|
4
|
+
};
|
|
5
|
+
type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>;
|
|
6
|
+
declare const UNSET: any;
|
|
7
|
+
/**
|
|
8
|
+
* Check whether a value is a Signal or not
|
|
9
|
+
*
|
|
10
|
+
* @since 0.9.0
|
|
11
|
+
* @param {unknown} value - value to check
|
|
12
|
+
* @returns {boolean} - true if value is a Signal, false otherwise
|
|
13
|
+
*/
|
|
14
|
+
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
|
+
/**
|
|
24
|
+
* Convert a value to a Signal if it's not already a Signal
|
|
25
|
+
*
|
|
26
|
+
* @since 0.9.6
|
|
27
|
+
* @param {MaybeSignal<T>} value - value to convert to a Signal
|
|
28
|
+
* @returns {Signal<T>} - converted Signal
|
|
29
|
+
*/
|
|
30
|
+
declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
|
|
31
|
+
export { type Signal, type MaybeSignal, UNSET, isSignal, isComputedCallback, toSignal, };
|