forked from proposal-signals/signal-polyfill
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomputed.ts
147 lines (125 loc) · 4.32 KB
/
computed.ts
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {defaultEquals, ValueEqualityFn} from './equality.js';
import {
consumerAfterComputation,
consumerBeforeComputation,
producerAccessed,
producerUpdateValueVersion,
REACTIVE_NODE,
ReactiveNode,
SIGNAL,
} from './graph.js';
/**
* A computation, which derives a value from a declarative reactive expression.
*
* `Computed`s are both producers and consumers of reactivity.
*/
export interface ComputedNode<T> extends ReactiveNode {
/**
* Current value of the computation, or one of the sentinel values above (`UNSET`, `COMPUTING`,
* `ERROR`).
*/
value: T;
/**
* If `value` is `ERRORED`, the error caught from the last computation attempt which will
* be re-thrown.
*/
error: unknown;
/**
* The computation function which will produce a new value.
*/
computation: () => T;
equal: ValueEqualityFn<T>;
}
export type ComputedGetter<T> = (() => T) & {
[SIGNAL]: ComputedNode<T>;
};
export function computedGet<T>(node: ComputedNode<T>) {
// Check if the value needs updating before returning it.
producerUpdateValueVersion(node);
// Record that someone looked at this signal.
producerAccessed(node);
if (node.value === ERRORED) {
throw node.error;
}
return node.value;
}
/**
* Create a computed signal which derives a reactive value from an expression.
*/
export function createComputed<T>(computation: () => T): ComputedGetter<T> {
const node: ComputedNode<T> = Object.create(COMPUTED_NODE);
node.computation = computation;
const computed = () => computedGet(node);
(computed as ComputedGetter<T>)[SIGNAL] = node;
return computed as unknown as ComputedGetter<T>;
}
/**
* A dedicated symbol used before a computed value has been calculated for the first time.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const UNSET: any = /* @__PURE__ */ Symbol('UNSET');
/**
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
* is in progress. Used to detect cycles in computation chains.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const COMPUTING: any = /* @__PURE__ */ Symbol('COMPUTING');
/**
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
* failed. The thrown error is cached until the computation gets dirty again.
* Explicitly typed as `any` so we can use it as signal's value.
*/
const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
const COMPUTED_NODE = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
value: UNSET,
dirty: true,
error: null,
equal: defaultEquals,
producerMustRecompute(node: ComputedNode<unknown>): boolean {
// Force a recomputation if there's no current value, or if the current value is in the
// process of being calculated (which should throw an error).
return node.value === UNSET || node.value === COMPUTING;
},
producerRecomputeValue(node: ComputedNode<unknown>): void {
if (node.value === COMPUTING) {
// Our computation somehow led to a cyclic read of itself.
throw new Error('Detected cycle in computations.');
}
const oldValue = node.value;
node.value = COMPUTING;
const prevConsumer = consumerBeforeComputation(node);
let newValue: unknown;
let wasEqual = false;
try {
newValue = node.computation.call(node.wrapper);
const oldOk = oldValue !== UNSET && oldValue !== ERRORED;
wasEqual = oldOk && node.equal.call(node.wrapper, oldValue, newValue);
} catch (err) {
newValue = ERRORED;
node.error = err;
} finally {
consumerAfterComputation(node, prevConsumer);
}
if (wasEqual) {
// No change to `valueVersion` - old and new values are
// semantically equivalent.
node.value = oldValue;
return;
}
node.value = newValue;
node.version++;
},
};
})();