Skip to content

Commit b29bc03

Browse files
feat: add polyfill and test source code
Still needs the actual test and build setup.
1 parent d75f64e commit b29bc03

11 files changed

+2124
-3
lines changed

packages/signal-polyfill/package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,11 @@
1010
"watch": "tsc -b tsconfig.json -watch"
1111
},
1212
"author": "EisenbergEffect",
13-
"license": "MIT"
13+
"license": "MIT",
14+
"dependencies": {
15+
"tslib": "latest"
16+
},
17+
"devDependencies": {
18+
"typescript": "latest"
19+
}
1420
}

packages/signal-polyfill/readme.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Signal Polyfill
22

3-
A "signal" is a proposed first-class JavaScript data type that enables one-way data flow through cells of state or computations derived from other data.
3+
A "signal" is a proposed first-class JavaScript data type that enables one-way data flow through cells of state or computations derived from other state/computations.
44

5-
This is a polyfill for the `Signal` API that makes these capabilities available in browsers that don't yet support them natively.
5+
This is a polyfill for the `Signal` API.
66

77
# Examples
88

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

0 commit comments

Comments
 (0)