Skip to content

Commit 323ebbe

Browse files
committed
Make signals interoperable
1 parent 1c33f91 commit 323ebbe

File tree

6 files changed

+530
-39
lines changed

6 files changed

+530
-39
lines changed

src/computed.ts

+14-2
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 {
2636
/**
2737
* Current value of the computation, or one of the sentinel values above (`UNSET`, `COMPUTING`,
2838
* `ERROR`).
@@ -94,9 +104,11 @@ const ERRORED: any = /* @__PURE__ */ Symbol('ERRORED');
94104
// Note: Using an IIFE here to ensure that the spread assignment is not considered
95105
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
96106
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
97-
const COMPUTED_NODE = /* @__PURE__ */ (() => {
107+
const COMPUTED_NODE: Omit<ComputedNode<unknown>, 'computation'> = /* @__PURE__ */ (() => {
98108
return {
99109
...REACTIVE_NODE,
110+
...PRODUCER_NODE,
111+
...CONSUMER_NODE,
100112
value: UNSET,
101113
dirty: true,
102114
error: null,

src/graph.ts

+43-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,
@@ -143,6 +134,11 @@ export interface ReactiveNode {
143134
*/
144135
nextProducerIndex: number;
145136

137+
/**
138+
* Whether this consumer has any interop signals as dependencies.
139+
*/
140+
hasInteropSignalDep: boolean;
141+
146142
/**
147143
* Array of consumers of this producer that are "live" (they require push notifications).
148144
*
@@ -180,6 +176,11 @@ export interface ReactiveNode {
180176
*/
181177
consumerOnSignalRead(node: unknown): void;
182178

179+
/**
180+
* Called when the signal is accessed.
181+
*/
182+
producerOnAccess?(): void;
183+
183184
/**
184185
* Called when the signal becomes "live"
185186
*/
@@ -211,27 +212,32 @@ interface ProducerNode extends ReactiveNode {
211212
/**
212213
* Called by implementations when a producer's signal is read.
213214
*/
214-
export function producerAccessed(node: ReactiveNode): void {
215+
export function producerAccessed<T>(node: ReactiveNode & InteropSignal): void {
215216
if (inNotificationPhase) {
216217
throw new Error(
217218
typeof ngDevMode !== 'undefined' && ngDevMode
218219
? `Assertion error: signal read during notification phase`
219220
: '',
220221
);
221222
}
223+
getActiveConsumer()?.addProducer(node);
224+
}
222225

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

230232
// This producer is the `idx`th dependency of `activeConsumer`.
231233
const idx = activeConsumer.nextProducerIndex++;
232234

233235
assertConsumerNode(activeConsumer);
234236

237+
if (node.hasInteropSignalDep) {
238+
activeConsumer.hasInteropSignalDep = true;
239+
}
240+
235241
if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
236242
// There's been a change in producers since the last execution of `activeConsumer`.
237243
// `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and
@@ -275,7 +281,7 @@ export function producerIncrementEpoch(): void {
275281
* Ensure this producer's `version` is up-to-date.
276282
*/
277283
export function producerUpdateValueVersion(node: ReactiveNode): void {
278-
if (!node.dirty && node.lastCleanEpoch === epoch) {
284+
if (!node.dirty && node.lastCleanEpoch === epoch && !node.hasInteropSignalDep) {
279285
// Even non-live consumers can skip polling if they previously found themselves to be clean at
280286
// the current epoch, since their dependencies could not possibly have changed (such a change
281287
// would've increased the epoch).
@@ -324,7 +330,10 @@ export function producerNotifyConsumers(node: ReactiveNode): void {
324330
* based on the current consumer context.
325331
*/
326332
export function producerUpdatesAllowed(): boolean {
327-
return activeConsumer?.consumerAllowSignalWrites !== false;
333+
return (
334+
(getActiveConsumer() as (InteropConsumer & ReactiveNode) | null)?.consumerAllowSignalWrites !==
335+
false
336+
);
328337
}
329338

330339
export function consumerMarkDirty(node: ReactiveNode): void {
@@ -339,8 +348,13 @@ export function consumerMarkDirty(node: ReactiveNode): void {
339348
* Must be called by subclasses which represent reactive computations, before those computations
340349
* begin.
341350
*/
342-
export function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNode | null {
343-
node && (node.nextProducerIndex = 0);
351+
export function consumerBeforeComputation(
352+
node: (ReactiveNode & InteropConsumer) | null,
353+
): InteropConsumer | null {
354+
if (node) {
355+
node.nextProducerIndex = 0;
356+
node.hasInteropSignalDep = false;
357+
}
344358
return setActiveConsumer(node);
345359
}
346360

@@ -352,7 +366,7 @@ export function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNo
352366
*/
353367
export function consumerAfterComputation(
354368
node: ReactiveNode | null,
355-
prevConsumer: ReactiveNode | null,
369+
prevConsumer: InteropConsumer | null,
356370
): void {
357371
setActiveConsumer(prevConsumer);
358372

0 commit comments

Comments
 (0)