Skip to content

Commit ba2bddd

Browse files
committed
chore(core): sync signal state with ngrx/signals
1 parent 6d7f0cb commit ba2bddd

File tree

1 file changed

+64
-28
lines changed

1 file changed

+64
-28
lines changed

libs/core/src/lib/utils/signal-state.ts

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** ported from ngrx/signals */
2-
import { computed, isSignal, Signal as NgSignal, signal, untracked, WritableSignal } from '@angular/core';
2+
/** Last synced: 08/16/2025 */
3+
import { computed, isSignal, type Signal, signal, untracked, type WritableSignal } from '@angular/core';
34

45
type NonRecord =
56
| Iterable<any>
@@ -20,50 +21,69 @@ type IsKnownRecord<T> = IsRecord<T> extends true ? (IsUnknownRecord<T> extends t
2021

2122
const STATE_SOURCE = Symbol('STATE_SOURCE');
2223

23-
type WritableStateSource<State extends object> = {
24-
[STATE_SOURCE]: WritableSignal<State>;
24+
export type WritableStateSource<State extends object> = {
25+
[STATE_SOURCE]: { [K in keyof State]: WritableSignal<State[K]> };
26+
};
27+
28+
export type StateSource<State extends object> = {
29+
[STATE_SOURCE]: { [K in keyof State]: Signal<State[K]> };
2530
};
2631

2732
export type PartialStateUpdater<State extends object> = (state: State) => Partial<State>;
2833

34+
function getState<State extends object>(stateSource: StateSource<State>): State {
35+
const signals: Record<string | symbol, Signal<unknown>> = stateSource[STATE_SOURCE];
36+
return Reflect.ownKeys(stateSource[STATE_SOURCE]).reduce((state, key) => {
37+
const value = signals[key]();
38+
return Object.assign(state, { [key]: value });
39+
}, {} as State);
40+
}
41+
2942
function patchState<State extends object>(
3043
stateSource: WritableStateSource<State>,
31-
...updaters: Array<Partial<Prettify<State>> | PartialStateUpdater<Prettify<State>>>
44+
...updaters: Array<Partial<NoInfer<State>> | PartialStateUpdater<NoInfer<State>>>
3245
): void {
33-
stateSource[STATE_SOURCE].update((currentState) =>
34-
updaters.reduce(
35-
(nextState: State, updater) => ({
36-
...nextState,
37-
...(typeof updater === 'function' ? updater(nextState) : updater),
38-
}),
39-
currentState,
40-
),
46+
const currentState = untracked(() => getState(stateSource));
47+
const newState = updaters.reduce(
48+
(nextState: State, updater) => ({
49+
...nextState,
50+
...(typeof updater === 'function' ? updater(nextState) : updater),
51+
}),
52+
currentState,
4153
);
42-
}
4354

44-
// An extended Signal type that enables the correct typing
45-
// of nested signals with the `name` or `length` key.
46-
export interface Signal<T> extends NgSignal<T> {
47-
name: unknown;
48-
length: unknown;
55+
const signals = stateSource[STATE_SOURCE];
56+
57+
for (const key of Reflect.ownKeys(newState)) {
58+
const signalKey = key as keyof State;
59+
60+
if (currentState[signalKey] !== newState[signalKey]) {
61+
signals[signalKey].set(newState[signalKey]);
62+
}
63+
}
4964
}
5065

66+
const DEEP_SIGNAL = Symbol('DEEP_SIGNAL');
67+
5168
export type DeepSignal<T> = Signal<T> &
5269
(IsKnownRecord<T> extends true
5370
? Readonly<{
5471
[K in keyof T]: IsKnownRecord<T[K]> extends true ? DeepSignal<T[K]> : Signal<T[K]>;
5572
}>
5673
: unknown);
5774

58-
function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
59-
const value = untracked(() => signal());
60-
if (!isRecord(value)) {
61-
return signal as DeepSignal<T>;
62-
}
63-
75+
export function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
6476
return new Proxy(signal, {
77+
has(target: any, prop) {
78+
return !!this.get!(target, prop, undefined);
79+
},
6580
get(target: any, prop) {
66-
if (!(prop in value)) {
81+
const value = untracked(target);
82+
if (!isRecord(value) || !(prop in value)) {
83+
if (isSignal(target[prop]) && (target[prop] as any)[DEEP_SIGNAL]) {
84+
delete target[prop];
85+
}
86+
6787
return target[prop];
6888
}
6989

@@ -72,6 +92,7 @@ function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
7292
value: computed(() => target()[prop]),
7393
configurable: true,
7494
});
95+
target[prop][DEEP_SIGNAL] = true;
7596
}
7697

7798
return toDeepSignal(target[prop]);
@@ -112,14 +133,29 @@ export type SignalState<State extends object> = DeepSignal<State> &
112133
};
113134

114135
export function signalState<State extends object>(initialState: State): SignalState<State> {
115-
const stateSource = signal(initialState as State);
116-
const signalState = toDeepSignal(stateSource.asReadonly());
136+
const stateKeys = Reflect.ownKeys(initialState);
137+
138+
const stateSource = stateKeys.reduce(
139+
(signalsDict, key) =>
140+
Object.assign(signalsDict, {
141+
[key]: signal((initialState as Record<string | symbol, unknown>)[key]),
142+
}),
143+
{} as Record<string | symbol, any>,
144+
);
145+
146+
const signalState = computed(() => stateKeys.reduce((state, key) => ({ ...state, [key]: stateSource[key]() }), {}));
117147

118148
Object.defineProperties(signalState, {
119149
[STATE_SOURCE]: { value: stateSource },
120150
update: { value: patchState.bind(null, signalState as SignalState<State>) },
121-
snapshot: { get: () => untracked(stateSource) },
151+
snapshot: { get: () => untracked(signalState) },
122152
});
123153

154+
for (const key of stateKeys) {
155+
Object.defineProperty(signalState, key, {
156+
value: toDeepSignal(stateSource[key]),
157+
});
158+
}
159+
124160
return signalState as SignalState<State>;
125161
}

0 commit comments

Comments
 (0)