forked from proposal-signals/signal-polyfill
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsignal.ts
105 lines (90 loc) · 2.97 KB
/
signal.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
/**
* @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, ValueEqualityComparer} from './equality.js';
import {throwInvalidWriteToSignalError} from './errors.js';
import {
producerAccessed,
producerIncrementEpoch,
producerNotifyConsumers,
producerUpdatesAllowed,
REACTIVE_NODE,
ReactiveNode,
SIGNAL,
} from './graph.js';
// Required as the signals library is in a separate package, so we need to explicitly ensure the
// global `ngDevMode` type is defined.
declare const ngDevMode: boolean | undefined;
/**
* If set, called after `WritableSignal`s are updated.
*
* This hook can be used to achieve various effects, such as running effects synchronously as part
* of setting a signal.
*/
let postSignalSetFn: (() => void) | null = null;
export interface SignalNode<T> extends ReactiveNode, ValueEqualityComparer<T> {
value: T;
}
export type SignalBaseGetter<T> = (() => T) & {readonly [SIGNAL]: unknown};
// Note: Closure *requires* this to be an `interface` and not a type, which is why the
// `SignalBaseGetter` type exists to provide the correct shape.
export interface SignalGetter<T> extends SignalBaseGetter<T> {
readonly [SIGNAL]: SignalNode<T>;
}
/**
* Create a `Signal` that can be set or updated directly.
*/
export function createSignal<T>(initialValue: T): SignalGetter<T> {
const node: SignalNode<T> = Object.create(SIGNAL_NODE);
node.value = initialValue;
const getter = (() => {
producerAccessed(node);
return node.value;
}) as SignalGetter<T>;
(getter as any)[SIGNAL] = node;
return getter;
}
export function setPostSignalSetFn(fn: (() => void) | null): (() => void) | null {
const prev = postSignalSetFn;
postSignalSetFn = fn;
return prev;
}
export function signalGetFn<T>(this: SignalNode<T>): T {
producerAccessed(this);
return this.value;
}
export function signalSetFn<T>(node: SignalNode<T>, newValue: T) {
if (!producerUpdatesAllowed()) {
throwInvalidWriteToSignalError();
}
if (!node.equal.call(node.wrapper, node.value, newValue)) {
node.value = newValue;
signalValueChanged(node);
}
}
export function signalUpdateFn<T>(node: SignalNode<T>, updater: (value: T) => T): void {
if (!producerUpdatesAllowed()) {
throwInvalidWriteToSignalError();
}
signalSetFn(node, updater(node.value));
}
// 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.
export const SIGNAL_NODE: SignalNode<unknown> = /* @__PURE__ */ (() => {
return {
...REACTIVE_NODE,
equal: defaultEquals,
value: undefined,
};
})();
function signalValueChanged<T>(node: SignalNode<T>): void {
node.version++;
producerIncrementEpoch();
producerNotifyConsumers(node);
postSignalSetFn?.();
}