Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions goldens/public-api/core/primitives/signals/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export interface Reactive {
// @public (undocumented)
export const REACTIVE_NODE: ReactiveNode;

// @public (undocumented)
export type ReactiveHookFn = (node: ReactiveNode) => void;

// @public
export interface ReactiveNode {
consumerAllowSignalWrites: boolean;
Expand All @@ -132,7 +135,10 @@ export interface ReactiveNode {
}

// @public (undocumented)
export function runPostSignalSetFn(): void;
export function runPostProducerCreatedFn(node: ReactiveNode): void;

// @public (undocumented)
export function runPostSignalSetFn<T>(node: SignalNode<T>): void;

// @public (undocumented)
export function setActiveConsumer(consumer: ReactiveNode | null): ReactiveNode | null;
Expand All @@ -141,7 +147,10 @@ export function setActiveConsumer(consumer: ReactiveNode | null): ReactiveNode |
export function setAlternateWeakRefImpl(impl: unknown): void;

// @public (undocumented)
export function setPostSignalSetFn(fn: (() => void) | null): (() => void) | null;
export function setPostProducerCreatedFn(fn: ReactiveHookFn | null): ReactiveHookFn | null;

// @public (undocumented)
export function setPostSignalSetFn(fn: ReactiveHookFn | null): ReactiveHookFn | null;

// @public (undocumented)
export function setThrowInvalidWriteToSignalError(fn: <T>(node: SignalNode<T>) => never): void;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/primitives/signals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {setThrowInvalidWriteToSignalError} from './src/errors';
export {
REACTIVE_NODE,
Reactive,
ReactiveHookFn,
ReactiveNode,
SIGNAL,
consumerAfterComputation,
Expand All @@ -36,7 +37,9 @@ export {
producerNotifyConsumers,
producerUpdateValueVersion,
producerUpdatesAllowed,
runPostProducerCreatedFn,
setActiveConsumer,
setPostProducerCreatedFn,
} from './src/graph';
export {
SIGNAL_NODE,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/primitives/signals/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ReactiveNode,
setActiveConsumer,
SIGNAL,
runPostProducerCreatedFn,
} from './graph';

/**
Expand Down Expand Up @@ -76,6 +77,7 @@ export function createComputed<T>(
return node.value;
};
(computed as ComputedGetter<T>)[SIGNAL] = node;
runPostProducerCreatedFn(node);
return computed as unknown as ComputedGetter<T>;
}

Expand Down
17 changes: 17 additions & 0 deletions packages/core/primitives/signals/src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ type Version = number & {__brand: 'Version'};
*/
let epoch: Version = 1 as Version;

export type ReactiveHookFn = (node: ReactiveNode) => void;

/**
* If set, called after a producer `ReactiveNode` is created.
*/
let postProducerCreatedFn: ReactiveHookFn | null = null;

/**
* Symbol used to tell `Signal`s apart from other functions.
*
Expand Down Expand Up @@ -527,3 +534,13 @@ function assertProducerNode(node: ReactiveNode): asserts node is ProducerNode {
function isConsumerNode(node: ReactiveNode): node is ConsumerNode {
return node.producerNode !== undefined;
}

export function runPostProducerCreatedFn(node: ReactiveNode): void {
postProducerCreatedFn?.(node);
}

export function setPostProducerCreatedFn(fn: ReactiveHookFn | null): ReactiveHookFn | null {
const prev = postProducerCreatedFn;
postProducerCreatedFn = fn;
return prev;
}
3 changes: 2 additions & 1 deletion packages/core/primitives/signals/src/linked_signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
producerUpdateValueVersion,
REACTIVE_NODE,
ReactiveNode,
runPostProducerCreatedFn,
SIGNAL,
} from './graph';
import {signalSetFn, signalUpdateFn} from './signal';
Expand Down Expand Up @@ -86,7 +87,7 @@ export function createLinkedSignal<S, D>(

const getter = linkedSignalGetter as LinkedSignalGetter<S, D>;
getter[SIGNAL] = node;

runPostProducerCreatedFn(node);
return getter;
}

Expand Down
13 changes: 8 additions & 5 deletions packages/core/primitives/signals/src/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
producerUpdatesAllowed,
REACTIVE_NODE,
ReactiveNode,
ReactiveHookFn,
runPostProducerCreatedFn,
SIGNAL,
} from './graph';

Expand All @@ -28,7 +30,7 @@ declare const ngDevMode: boolean | undefined;
* 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;
let postSignalSetFn: ReactiveHookFn | null = null;

export interface SignalNode<T> extends ReactiveNode {
value: T;
Expand Down Expand Up @@ -57,10 +59,11 @@ export function createSignal<T>(initialValue: T, equal?: ValueEqualityFn<T>): Si
return node.value;
}) as SignalGetter<T>;
(getter as any)[SIGNAL] = node;
runPostProducerCreatedFn(node);
return getter;
}

export function setPostSignalSetFn(fn: (() => void) | null): (() => void) | null {
export function setPostSignalSetFn(fn: ReactiveHookFn | null): ReactiveHookFn | null {
const prev = postSignalSetFn;
postSignalSetFn = fn;
return prev;
Expand Down Expand Up @@ -90,8 +93,8 @@ export function signalUpdateFn<T>(node: SignalNode<T>, updater: (value: T) => T)
signalSetFn(node, updater(node.value));
}

export function runPostSignalSetFn(): void {
postSignalSetFn?.();
export function runPostSignalSetFn<T>(node: SignalNode<T>): void {
postSignalSetFn?.(node);
}

// Note: Using an IIFE here to ensure that the spread assignment is not considered
Expand All @@ -110,5 +113,5 @@ function signalValueChanged<T>(node: SignalNode<T>): void {
node.version++;
producerIncrementEpoch();
producerNotifyConsumers(node);
postSignalSetFn?.();
postSignalSetFn?.(node);
}
18 changes: 17 additions & 1 deletion packages/core/test/signals/computed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
*/

import {computed, signal} from '@angular/core';
import {createWatch, ReactiveNode, SIGNAL, defaultEquals} from '@angular/core/primitives/signals';
import {
createWatch,
ReactiveNode,
SIGNAL,
defaultEquals,
setPostProducerCreatedFn,
} from '@angular/core/primitives/signals';

describe('computed', () => {
it('should create computed', () => {
Expand Down Expand Up @@ -317,4 +323,14 @@ describe('computed', () => {
expect(derived()).toBe(2);
});
});

it('should call the post-producer-created fn when signal is called', () => {
const producerKindsCreated: string[] = [];
const prev = setPostProducerCreatedFn((node) => producerKindsCreated.push(node.kind));
const count = signal(0);
computed(() => count() % 2 === 0);

expect(producerKindsCreated).toEqual(['signal', 'computed']);
setPostProducerCreatedFn(prev);
});
});
11 changes: 11 additions & 0 deletions packages/core/test/signals/linked_signal_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {isSignal, linkedSignal, signal, computed} from '@angular/core';
import {setPostProducerCreatedFn} from '@angular/core/primitives/signals';
import {testingEffect} from './effect_util';

describe('linkedSignal', () => {
Expand Down Expand Up @@ -275,4 +276,14 @@ describe('linkedSignal', () => {
choice.set('explicit');
expect(choice()).toBe('explicit');
});

it('should call the post-producer-created fn when signal is called', () => {
let producers = 0;
const prev = setPostProducerCreatedFn(() => producers++);
const options = signal(['apple', 'banana', 'fig']);
linkedSignal(() => options()[0]);

expect(producers).toBe(2);
setPostProducerCreatedFn(prev);
});
});
30 changes: 28 additions & 2 deletions packages/core/test/signals/signal_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
*/

import {computed, signal} from '@angular/core';
import {ReactiveNode, setPostSignalSetFn, SIGNAL} from '@angular/core/primitives/signals';
import {
ReactiveHookFn,
ReactiveNode,
setPostProducerCreatedFn,
setPostSignalSetFn,
SIGNAL,
} from '@angular/core/primitives/signals';

describe('signals', () => {
it('should be a getter which reflects the set value', () => {
Expand Down Expand Up @@ -166,7 +172,7 @@ describe('signals', () => {
});

describe('post-signal-set functions', () => {
let prevPostSignalSetFn: (() => void) | null = null;
let prevPostSignalSetFn: ReactiveHookFn | null = null;
let log: number;
beforeEach(() => {
log = 0;
Expand Down Expand Up @@ -197,5 +203,25 @@ describe('signals', () => {
counter.set(0);
expect(log).toBe(0);
});

it('should pass post-signal-set fn the node that was updated', () => {
const counter = signal(0, {debugName: 'test-signal'});
let node: ReactiveNode | null = null;
setPostSignalSetFn((n: ReactiveNode) => {
node = n;
});

counter.set(1);
expect(node!.debugName).toBe('test-signal');
});
});

it('should call the post-producer-created fn when signal is called', () => {
const producerKindsCreated: string[] = [];
const prev = setPostProducerCreatedFn((node) => producerKindsCreated.push(node.kind));
signal(0);

expect(producerKindsCreated).toEqual(['signal']);
setPostProducerCreatedFn(prev);
});
});