Skip to content

Commit cb1a2ba

Browse files
fix(signals): allow lazy initialization of DeepSignal (#4866)
Closes #4749
1 parent 734678e commit cb1a2ba

File tree

2 files changed

+122
-37
lines changed

2 files changed

+122
-37
lines changed

modules/signals/spec/deep-signal.spec.ts

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,42 @@ describe('toDeepSignal', () => {
4242
expect(deepSig.user.firstName()).toBe('John');
4343
});
4444

45-
it('does not create deep signals for primitives', () => {
45+
it('allows lazy initialization', () => {
46+
const sig = signal(undefined as unknown as { m: { s: 't' } });
47+
const deepSig = toDeepSignal(sig);
48+
49+
sig.set({ m: { s: 't' } });
50+
51+
expect(deepSig()).toEqual({ m: { s: 't' } });
52+
expect(deepSig.m()).toEqual({ s: 't' });
53+
expect(deepSig.m.s()).toBe('t');
54+
});
55+
56+
it('creates a deep signal when value is a union of objects', () => {
57+
const sig = signal({ m: { s: 't' } } as
58+
| { s: 'asdf' }
59+
| { m: { s: string } });
60+
const deepSig = toDeepSignal(sig);
61+
62+
expect('m' in deepSig).toBe(true);
63+
expect('m' in deepSig && deepSig.m()).toEqual({ s: 't' });
64+
expect('m' in deepSig && deepSig.m.s()).toBe('t');
65+
66+
sig.set({ s: 'asdf' });
67+
68+
expect('m' in deepSig).toBe(false);
69+
expect('s' in deepSig).toBe(true);
70+
expect('s' in deepSig && deepSig.s()).toBe('asdf');
71+
72+
sig.set({ m: { s: 'ngrx' } });
73+
74+
expect('s' in deepSig).toBe(false);
75+
expect('m' in deepSig).toBe(true);
76+
expect('m' in deepSig && deepSig.m()).toEqual({ s: 'ngrx' });
77+
expect('m' in deepSig && deepSig.m.s()).toBe('ngrx');
78+
});
79+
80+
it('does not affect signals with primitives as values', () => {
4681
const num = signal(0);
4782
const str = signal('str');
4883
const bool = signal(true);
@@ -51,12 +86,17 @@ describe('toDeepSignal', () => {
5186
const deepStr = toDeepSignal(str);
5287
const deepBool = toDeepSignal(bool);
5388

54-
expect(deepNum).toBe(num);
55-
expect(deepStr).toBe(str);
56-
expect(deepBool).toBe(bool);
89+
expect(isSignal(deepNum)).toBe(true);
90+
expect(deepNum()).toBe(num());
91+
92+
expect(isSignal(deepStr)).toBe(true);
93+
expect(deepStr()).toBe(str());
94+
95+
expect(isSignal(deepBool)).toBe(true);
96+
expect(deepBool()).toBe(bool());
5797
});
5898

59-
it('does not create deep signals for iterables', () => {
99+
it('does not affect signals with iterables as values', () => {
60100
const array = signal([]);
61101
const set = signal(new Set());
62102
const map = signal(new Map());
@@ -69,14 +109,23 @@ describe('toDeepSignal', () => {
69109
const deepUintArray = toDeepSignal(uintArray);
70110
const deepFloatArray = toDeepSignal(floatArray);
71111

72-
expect(deepArray).toBe(array);
73-
expect(deepSet).toBe(set);
74-
expect(deepMap).toBe(map);
75-
expect(deepUintArray).toBe(uintArray);
76-
expect(deepFloatArray).toBe(floatArray);
112+
expect(isSignal(deepArray)).toBe(true);
113+
expect(deepArray()).toBe(array());
114+
115+
expect(isSignal(deepSet)).toBe(true);
116+
expect(deepSet()).toBe(set());
117+
118+
expect(isSignal(deepMap)).toBe(true);
119+
expect(deepMap()).toBe(map());
120+
121+
expect(isSignal(deepUintArray)).toBe(true);
122+
expect(deepUintArray()).toBe(uintArray());
123+
124+
expect(isSignal(deepFloatArray)).toBe(true);
125+
expect(deepFloatArray()).toBe(floatArray());
77126
});
78127

79-
it('does not create deep signals for built-in object types', () => {
128+
it('does not affect signals with built-in object types as values', () => {
80129
const weakSet = signal(new WeakSet());
81130
const weakMap = signal(new WeakMap());
82131
const promise = signal(Promise.resolve(10));
@@ -95,17 +144,32 @@ describe('toDeepSignal', () => {
95144
const deepArrayBuffer = toDeepSignal(arrayBuffer);
96145
const deepDataView = toDeepSignal(dataView);
97146

98-
expect(deepWeakSet).toBe(weakSet);
99-
expect(deepWeakMap).toBe(weakMap);
100-
expect(deepPromise).toBe(promise);
101-
expect(deepDate).toBe(date);
102-
expect(deepError).toBe(error);
103-
expect(deepRegExp).toBe(regExp);
104-
expect(deepArrayBuffer).toBe(arrayBuffer);
105-
expect(deepDataView).toBe(dataView);
147+
expect(isSignal(deepWeakSet)).toBe(true);
148+
expect(deepWeakSet()).toBe(weakSet());
149+
150+
expect(isSignal(deepWeakMap)).toBe(true);
151+
expect(deepWeakMap()).toBe(weakMap());
152+
153+
expect(isSignal(deepPromise)).toBe(true);
154+
expect(deepPromise()).toBe(promise());
155+
156+
expect(isSignal(deepDate)).toBe(true);
157+
expect(deepDate()).toBe(date());
158+
159+
expect(isSignal(deepError)).toBe(true);
160+
expect(deepError()).toBe(error());
161+
162+
expect(isSignal(deepRegExp)).toBe(true);
163+
expect(deepRegExp()).toBe(regExp());
164+
165+
expect(isSignal(deepArrayBuffer)).toBe(true);
166+
expect(deepArrayBuffer()).toBe(arrayBuffer());
167+
168+
expect(isSignal(deepDataView)).toBe(true);
169+
expect(deepDataView()).toBe(dataView());
106170
});
107171

108-
it('does not create deep signals for functions', () => {
172+
it('does not affect signals with functions as values', () => {
109173
const fn1 = signal(new Function());
110174
const fn2 = signal(function () {});
111175
const fn3 = signal(() => {});
@@ -114,12 +178,17 @@ describe('toDeepSignal', () => {
114178
const deepFn2 = toDeepSignal(fn2);
115179
const deepFn3 = toDeepSignal(fn3);
116180

117-
expect(deepFn1).toBe(fn1);
118-
expect(deepFn2).toBe(fn2);
119-
expect(deepFn3).toBe(fn3);
181+
expect(isSignal(deepFn1)).toBe(true);
182+
expect(deepFn1()).toBe(fn1());
183+
184+
expect(isSignal(deepFn2)).toBe(true);
185+
expect(deepFn2()).toBe(fn2());
186+
187+
expect(isSignal(deepFn3)).toBe(true);
188+
expect(deepFn3()).toBe(fn3());
120189
});
121190

122-
it('does not create deep signals for custom class instances that are iterables', () => {
191+
it('does not affect signals with custom class instances that are iterables as values', () => {
123192
class CustomArray extends Array {}
124193

125194
class CustomSet extends Set {}
@@ -134,12 +203,17 @@ describe('toDeepSignal', () => {
134203
const deepFloatArray = toDeepSignal(floatArray);
135204
const deepSet = toDeepSignal(set);
136205

137-
expect(deepArray).toBe(array);
138-
expect(deepFloatArray).toBe(floatArray);
139-
expect(deepSet).toBe(set);
206+
expect(isSignal(deepArray)).toBe(true);
207+
expect(deepArray()).toBe(array());
208+
209+
expect(isSignal(deepFloatArray)).toBe(true);
210+
expect(deepFloatArray()).toBe(floatArray());
211+
212+
expect(isSignal(deepSet)).toBe(true);
213+
expect(deepSet()).toBe(set());
140214
});
141215

142-
it('does not create deep signals for custom class instances that extend built-in object types', () => {
216+
it('does not affect signals with custom class instances that extend built-in object types as values', () => {
143217
class CustomWeakMap extends WeakMap {}
144218

145219
class CustomError extends Error {}
@@ -154,8 +228,13 @@ describe('toDeepSignal', () => {
154228
const deepError = toDeepSignal(error);
155229
const deepArrayBuffer = toDeepSignal(arrayBuffer);
156230

157-
expect(deepWeakMap).toBe(weakMap);
158-
expect(deepError).toBe(error);
159-
expect(deepArrayBuffer).toBe(arrayBuffer);
231+
expect(isSignal(deepWeakMap)).toBe(true);
232+
expect(deepWeakMap()).toBe(weakMap());
233+
234+
expect(isSignal(deepError)).toBe(true);
235+
expect(deepError()).toBe(error());
236+
237+
expect(isSignal(deepArrayBuffer)).toBe(true);
238+
expect(deepArrayBuffer()).toBe(arrayBuffer());
160239
});
161240
});

modules/signals/src/deep-signal.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface Signal<T> extends NgSignal<T> {
1313
length: unknown;
1414
}
1515

16+
const DEEP_SIGNAL = Symbol('DEEP_SIGNAL');
17+
1618
export type DeepSignal<T> = Signal<T> &
1719
(IsKnownRecord<T> extends true
1820
? Readonly<{
@@ -23,14 +25,17 @@ export type DeepSignal<T> = Signal<T> &
2325
: unknown);
2426

2527
export function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
26-
const value = untracked(() => signal());
27-
if (!isRecord(value)) {
28-
return signal as DeepSignal<T>;
29-
}
30-
3128
return new Proxy(signal, {
29+
has(target: any, prop) {
30+
return !!this.get!(target, prop, undefined);
31+
},
3232
get(target: any, prop) {
33-
if (!(prop in value)) {
33+
const value = untracked(target);
34+
if (!isRecord(value) || !(prop in value)) {
35+
if (isSignal(target[prop]) && (target[prop] as any)[DEEP_SIGNAL]) {
36+
delete target[prop];
37+
}
38+
3439
return target[prop];
3540
}
3641

@@ -39,6 +44,7 @@ export function toDeepSignal<T>(signal: Signal<T>): DeepSignal<T> {
3944
value: computed(() => target()[prop]),
4045
configurable: true,
4146
});
47+
target[prop][DEEP_SIGNAL] = true;
4248
}
4349

4450
return toDeepSignal(target[prop]);

0 commit comments

Comments
 (0)