Skip to content

Commit 6483621

Browse files
refactor(core): move linkedSignal implementation to primitives (#59501)
This change refactors the Angular-specific linkedSignal implementation such that its core logic can be shared with other frameworks. PR Close #59501
1 parent 2ec826d commit 6483621

5 files changed

Lines changed: 208 additions & 3 deletions

File tree

goldens/public-api/core/primitives/signals/index.api.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
55
```ts
66

7+
// @public
8+
export type ComputationFn<S, D> = (source: S, previous?: {
9+
source: S;
10+
value: D;
11+
}) => D;
12+
713
// @public
814
export interface ComputedNode<T> extends ReactiveNode {
915
computation: () => T;
@@ -31,6 +37,9 @@ export function consumerPollProducersForChange(node: ReactiveNode): boolean;
3137
// @public
3238
export function createComputed<T>(computation: () => T): ComputedGetter<T>;
3339

40+
// @public (undocumented)
41+
export function createLinkedSignal<S, D>(sourceFn: () => S, computationFn: ComputationFn<S, D>, equalityFn?: ValueEqualityFn<D>): LinkedSignalGetter<S, D>;
42+
3443
// @public
3544
export function createSignal<T>(initialValue: T): SignalGetter<T>;
3645

@@ -49,6 +58,28 @@ export function isInNotificationPhase(): boolean;
4958
// @public (undocumented)
5059
export function isReactive(value: unknown): value is Reactive;
5160

61+
// @public (undocumented)
62+
export type LinkedSignalGetter<S, D> = (() => D) & {
63+
[SIGNAL]: LinkedSignalNode<S, D>;
64+
};
65+
66+
// @public (undocumented)
67+
export interface LinkedSignalNode<S, D> extends ReactiveNode {
68+
computation: ComputationFn<S, D>;
69+
// (undocumented)
70+
equal: ValueEqualityFn<D>;
71+
error: unknown;
72+
source: () => S;
73+
sourceValue: S;
74+
value: D;
75+
}
76+
77+
// @public (undocumented)
78+
export function linkedSignalSetFn<S, D>(node: LinkedSignalNode<S, D>, newValue: D): void;
79+
80+
// @public (undocumented)
81+
export function linkedSignalUpdateFn<S, D>(node: LinkedSignalNode<S, D>, updater: (value: D) => D): void;
82+
5283
// @public
5384
export function producerAccessed(node: ReactiveNode): void;
5485

packages/core/primitives/signals/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
*/
88

99
export {ComputedNode, createComputed} from './src/computed';
10+
export {
11+
ComputationFn,
12+
LinkedSignalNode,
13+
LinkedSignalGetter,
14+
createLinkedSignal,
15+
linkedSignalSetFn,
16+
linkedSignalUpdateFn,
17+
} from './src/linked_signal';
1018
export {ValueEqualityFn, defaultEquals} from './src/equality';
1119
export {setThrowInvalidWriteToSignalError} from './src/errors';
1220
export {

packages/core/primitives/signals/src/computed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,21 @@ export function createComputed<T>(computation: () => T): ComputedGetter<T> {
7676
* A dedicated symbol used before a computed value has been calculated for the first time.
7777
* Explicitly typed as `any` so we can use it as signal's value.
7878
*/
79-
const UNSET: any = /* @__PURE__ */ Symbol('UNSET');
79+
export const UNSET: any = /* @__PURE__ */ Symbol('UNSET');
8080

8181
/**
8282
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
8383
* is in progress. Used to detect cycles in computation chains.
8484
* Explicitly typed as `any` so we can use it as signal's value.
8585
*/
86-
const COMPUTING: any = /* @__PURE__ */ Symbol('COMPUTING');
86+
export const COMPUTING: any = /* @__PURE__ */ Symbol('COMPUTING');
8787

8888
/**
8989
* A dedicated symbol used in place of a computed signal value to indicate that a given computation
9090
* failed. The thrown error is cached until the computation gets dirty again.
9191
* Explicitly typed as `any` so we can use it as signal's value.
9292
*/
93-
const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
93+
export const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
9494

9595
// Note: Using an IIFE here to ensure that the spread assignment is not considered
9696
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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.dev/license
7+
*/
8+
9+
import {COMPUTING, ERRORED, UNSET} from './computed';
10+
import {defaultEquals, ValueEqualityFn} from './equality';
11+
import {
12+
consumerAfterComputation,
13+
consumerBeforeComputation,
14+
producerAccessed,
15+
producerMarkClean,
16+
producerUpdateValueVersion,
17+
REACTIVE_NODE,
18+
ReactiveNode,
19+
SIGNAL,
20+
} from './graph';
21+
import {signalSetFn, signalUpdateFn} from './signal';
22+
23+
export type ComputationFn<S, D> = (source: S, previous?: {source: S; value: D}) => D;
24+
25+
export interface LinkedSignalNode<S, D> extends ReactiveNode {
26+
/**
27+
* Value of the source signal that was used to derive the computed value.
28+
*/
29+
sourceValue: S;
30+
31+
/**
32+
* Current state value, or one of the sentinel values (`UNSET`, `COMPUTING`,
33+
* `ERROR`).
34+
*/
35+
value: D;
36+
37+
/**
38+
* If `value` is `ERRORED`, the error caught from the last computation attempt which will
39+
* be re-thrown.
40+
*/
41+
error: unknown;
42+
43+
/**
44+
* The source function represents reactive dependency based on which the linked state is reset.
45+
*/
46+
source: () => S;
47+
48+
/**
49+
* The computation function which will produce a new value based on the source and, optionally - previous values.
50+
*/
51+
computation: ComputationFn<S, D>;
52+
53+
equal: ValueEqualityFn<D>;
54+
}
55+
56+
export type LinkedSignalGetter<S, D> = (() => D) & {
57+
[SIGNAL]: LinkedSignalNode<S, D>;
58+
};
59+
60+
export function createLinkedSignal<S, D>(
61+
sourceFn: () => S,
62+
computationFn: ComputationFn<S, D>,
63+
equalityFn?: ValueEqualityFn<D>,
64+
): LinkedSignalGetter<S, D> {
65+
const node: LinkedSignalNode<S, D> = Object.create(LINKED_SIGNAL_NODE);
66+
67+
node.source = sourceFn;
68+
node.computation = computationFn;
69+
if (equalityFn != undefined) {
70+
node.equal = equalityFn;
71+
}
72+
73+
const linkedSignalGetter = () => {
74+
// Check if the value needs updating before returning it.
75+
producerUpdateValueVersion(node);
76+
77+
// Record that someone looked at this signal.
78+
producerAccessed(node);
79+
80+
if (node.value === ERRORED) {
81+
throw node.error;
82+
}
83+
84+
return node.value;
85+
};
86+
87+
const getter = linkedSignalGetter as LinkedSignalGetter<S, D>;
88+
getter[SIGNAL] = node;
89+
90+
return getter;
91+
}
92+
93+
export function linkedSignalSetFn<S, D>(node: LinkedSignalNode<S, D>, newValue: D) {
94+
producerUpdateValueVersion(node);
95+
signalSetFn(node, newValue);
96+
producerMarkClean(node);
97+
}
98+
99+
export function linkedSignalUpdateFn<S, D>(
100+
node: LinkedSignalNode<S, D>,
101+
updater: (value: D) => D,
102+
): void {
103+
producerUpdateValueVersion(node);
104+
signalUpdateFn(node, updater);
105+
producerMarkClean(node);
106+
}
107+
108+
// Note: Using an IIFE here to ensure that the spread assignment is not considered
109+
// a side-effect, ending up preserving `LINKED_SIGNAL_NODE` and `REACTIVE_NODE`.
110+
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
111+
export const LINKED_SIGNAL_NODE = /* @__PURE__ */ (() => {
112+
return {
113+
...REACTIVE_NODE,
114+
value: UNSET,
115+
dirty: true,
116+
error: null,
117+
equal: defaultEquals,
118+
119+
producerMustRecompute(node: LinkedSignalNode<unknown, unknown>): boolean {
120+
// Force a recomputation if there's no current value, or if the current value is in the
121+
// process of being calculated (which should throw an error).
122+
return node.value === UNSET || node.value === COMPUTING;
123+
},
124+
125+
producerRecomputeValue(node: LinkedSignalNode<unknown, unknown>): void {
126+
if (node.value === COMPUTING) {
127+
// Our computation somehow led to a cyclic read of itself.
128+
throw new Error('Detected cycle in computations.');
129+
}
130+
131+
const oldValue = node.value;
132+
node.value = COMPUTING;
133+
134+
const prevConsumer = consumerBeforeComputation(node);
135+
let newValue: unknown;
136+
try {
137+
const newSourceValue = node.source();
138+
const prev =
139+
oldValue === UNSET || oldValue === ERRORED
140+
? undefined
141+
: {
142+
source: node.sourceValue,
143+
value: oldValue,
144+
};
145+
newValue = node.computation(newSourceValue, prev);
146+
node.sourceValue = newSourceValue;
147+
} catch (err) {
148+
newValue = ERRORED;
149+
node.error = err;
150+
} finally {
151+
consumerAfterComputation(node, prevConsumer);
152+
}
153+
154+
if (oldValue !== UNSET && newValue !== ERRORED && node.equal(oldValue, newValue)) {
155+
// No change to `valueVersion` - old and new values are
156+
// semantically equivalent.
157+
node.value = oldValue;
158+
return;
159+
}
160+
161+
node.value = newValue;
162+
node.version++;
163+
},
164+
};
165+
})();

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@
572572
"init_let_declaration",
573573
"init_lift",
574574
"init_linked_signal",
575+
"init_linked_signal2",
575576
"init_linker",
576577
"init_list_reconciliation",
577578
"init_listener",

0 commit comments

Comments
 (0)