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
45type NonRecord =
56 | Iterable < any >
@@ -20,50 +21,69 @@ type IsKnownRecord<T> = IsRecord<T> extends true ? (IsUnknownRecord<T> extends t
2021
2122const 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
2732export 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+
2942function 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+
5168export 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
114135export 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