Skip to content

Commit 08ec792

Browse files
committed
Make signals interoperable
1 parent 1c33f91 commit 08ec792

File tree

6 files changed

+562
-38
lines changed

6 files changed

+562
-38
lines changed

src/computed.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {
10+
Consumer as InteropConsumer,
11+
Signal as InteropSignal,
12+
setActiveConsumer,
13+
} from './interop_lib';
914
import {defaultEquals, ValueEqualityComparer} from './equality.js';
1015
import {
1116
consumerAfterComputation,
@@ -16,13 +21,18 @@ import {
1621
ReactiveNode,
1722
SIGNAL,
1823
} from './graph.js';
24+
import {CONSUMER_NODE, PRODUCER_NODE} from './interop';
1925

2026
/**
2127
* A computation, which derives a value from a declarative reactive expression.
2228
*
2329
* `Computed`s are both producers and consumers of reactivity.
2430
*/
25-
export interface ComputedNode<T> extends ReactiveNode, ValueEqualityComparer<T> {
31+
export interface ComputedNode<T>
32+
extends ReactiveNode,
33+
ValueEqualityComparer<T>,
34+
InteropConsumer,
35+
InteropSignal<T> {
2636
/**
2737
* Current value of the computation, or one of the sentinel values above (`UNSET`, `COMPUTING`,
2838
* `ERROR`).
@@ -97,11 +107,20 @@ const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
97107
const COMPUTED_NODE = /* @__PURE__ */ (() => {
98108
return {
99109
...REACTIVE_NODE,
110+
...PRODUCER_NODE,
111+
...CONSUMER_NODE,
100112
value: UNSET,
101113
dirty: true,
102114
error: null,
103115
equal: defaultEquals,
104116

117+
producerValue: (node: ComputedNode<unknown>) => {
118+
if (node.value === ERRORED) {
119+
throw node.error;
120+
}
121+
return node.value;
122+
},
123+
105124
producerMustRecompute(node: ComputedNode<unknown>): boolean {
106125
// Force a recomputation if there's no current value, or if the current value is in the
107126
// process of being calculated (which should throw an error).

src/graph.ts

+50-29
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {
10+
getActiveConsumer,
11+
Consumer as InteropConsumer,
12+
Signal as InteropSignal,
13+
setActiveConsumer,
14+
} from './interop_lib';
15+
916
// Required as the signals library is in a separate package, so we need to explicitly ensure the
1017
// global `ngDevMode` type is defined.
1118
declare const ngDevMode: boolean | undefined;
1219

13-
/**
14-
* The currently active consumer `ReactiveNode`, if running code in a reactive context.
15-
*
16-
* Change this via `setActiveConsumer`.
17-
*/
18-
let activeConsumer: ReactiveNode | null = null;
1920
let inNotificationPhase = false;
2021

21-
type Version = number & {__brand: 'Version'};
22+
export type Version = number & {__brand: 'Version'};
2223

2324
/**
2425
* Global epoch counter. Incremented whenever a source signal is set.
@@ -32,16 +33,6 @@ let epoch: Version = 1 as Version;
3233
*/
3334
export const SIGNAL = /* @__PURE__ */ Symbol('SIGNAL');
3435

35-
export function setActiveConsumer(consumer: ReactiveNode | null): ReactiveNode | null {
36-
const prev = activeConsumer;
37-
activeConsumer = consumer;
38-
return prev;
39-
}
40-
41-
export function getActiveConsumer(): ReactiveNode | null {
42-
return activeConsumer;
43-
}
44-
4536
export function isInNotificationPhase(): boolean {
4637
return inNotificationPhase;
4738
}
@@ -53,7 +44,6 @@ export interface Reactive {
5344
export function isReactive(value: unknown): value is Reactive {
5445
return (value as Partial<Reactive>)[SIGNAL] !== undefined;
5546
}
56-
5747
export const REACTIVE_NODE: ReactiveNode = {
5848
version: 0 as Version,
5949
lastCleanEpoch: 0 as Version,
@@ -62,6 +52,7 @@ export const REACTIVE_NODE: ReactiveNode = {
6252
producerLastReadVersion: undefined,
6353
producerIndexOfThis: undefined,
6454
nextProducerIndex: 0,
55+
hasInteropSignalDep: false,
6556
liveConsumerNode: undefined,
6657
liveConsumerIndexOfThis: undefined,
6758
consumerAllowSignalWrites: false,
@@ -70,6 +61,7 @@ export const REACTIVE_NODE: ReactiveNode = {
7061
producerRecomputeValue: () => {},
7162
consumerMarkedDirty: () => {},
7263
consumerOnSignalRead: () => {},
64+
producerValue: () => {},
7365
};
7466

7567
/**
@@ -143,6 +135,11 @@ export interface ReactiveNode {
143135
*/
144136
nextProducerIndex: number;
145137

138+
/**
139+
* Whether this consumer has any interop signals as dependencies.
140+
*/
141+
hasInteropSignalDep: boolean;
142+
146143
/**
147144
* Array of consumers of this producer that are "live" (they require push notifications).
148145
*
@@ -180,6 +177,17 @@ export interface ReactiveNode {
180177
*/
181178
consumerOnSignalRead(node: unknown): void;
182179

180+
/**
181+
* Return the current value of the producer (may throw an error if it is the cached value).
182+
* Does not recompute the value.
183+
*/
184+
producerValue(): unknown;
185+
186+
/**
187+
* Called when the signal is accessed.
188+
*/
189+
producerOnAccess?(): void;
190+
183191
/**
184192
* Called when the signal becomes "live"
185193
*/
@@ -211,27 +219,32 @@ interface ProducerNode extends ReactiveNode {
211219
/**
212220
* Called by implementations when a producer's signal is read.
213221
*/
214-
export function producerAccessed(node: ReactiveNode): void {
222+
export function producerAccessed<T>(node: ReactiveNode & InteropSignal<T>): void {
215223
if (inNotificationPhase) {
216224
throw new Error(
217225
typeof ngDevMode !== 'undefined' && ngDevMode
218226
? `Assertion error: signal read during notification phase`
219227
: '',
220228
);
221229
}
230+
getActiveConsumer()?.addProducer(node);
231+
}
222232

223-
if (activeConsumer === null) {
224-
// Accessed outside of a reactive context, so nothing to record.
225-
return;
226-
}
227-
233+
export function internalProducerAccessed(
234+
node: ReactiveNode,
235+
activeConsumer: ReactiveNode & InteropConsumer,
236+
): void {
228237
activeConsumer.consumerOnSignalRead(node);
229238

230239
// This producer is the `idx`th dependency of `activeConsumer`.
231240
const idx = activeConsumer.nextProducerIndex++;
232241

233242
assertConsumerNode(activeConsumer);
234243

244+
if (node.hasInteropSignalDep) {
245+
activeConsumer.hasInteropSignalDep = true;
246+
}
247+
235248
if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
236249
// There's been a change in producers since the last execution of `activeConsumer`.
237250
// `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and
@@ -275,7 +288,7 @@ export function producerIncrementEpoch(): void {
275288
* Ensure this producer's `version` is up-to-date.
276289
*/
277290
export function producerUpdateValueVersion(node: ReactiveNode): void {
278-
if (!node.dirty && node.lastCleanEpoch === epoch) {
291+
if (!node.dirty && node.lastCleanEpoch === epoch && !node.hasInteropSignalDep) {
279292
// Even non-live consumers can skip polling if they previously found themselves to be clean at
280293
// the current epoch, since their dependencies could not possibly have changed (such a change
281294
// would've increased the epoch).
@@ -324,7 +337,10 @@ export function producerNotifyConsumers(node: ReactiveNode): void {
324337
* based on the current consumer context.
325338
*/
326339
export function producerUpdatesAllowed(): boolean {
327-
return activeConsumer?.consumerAllowSignalWrites !== false;
340+
return (
341+
(getActiveConsumer() as (InteropConsumer & ReactiveNode) | null)?.consumerAllowSignalWrites !==
342+
false
343+
);
328344
}
329345

330346
export function consumerMarkDirty(node: ReactiveNode): void {
@@ -339,8 +355,13 @@ export function consumerMarkDirty(node: ReactiveNode): void {
339355
* Must be called by subclasses which represent reactive computations, before those computations
340356
* begin.
341357
*/
342-
export function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNode | null {
343-
node && (node.nextProducerIndex = 0);
358+
export function consumerBeforeComputation(
359+
node: (ReactiveNode & InteropConsumer) | null,
360+
): InteropConsumer | null {
361+
if (node) {
362+
node.nextProducerIndex = 0;
363+
node.hasInteropSignalDep = false;
364+
}
344365
return setActiveConsumer(node);
345366
}
346367

@@ -352,7 +373,7 @@ export function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNo
352373
*/
353374
export function consumerAfterComputation(
354375
node: ReactiveNode | null,
355-
prevConsumer: ReactiveNode | null,
376+
prevConsumer: InteropConsumer | null,
356377
): void {
357378
setActiveConsumer(prevConsumer);
358379

0 commit comments

Comments
 (0)