|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import {computed, signal} from '@angular/core'; |
10 | | -import {createWatch, ReactiveNode, SIGNAL} from '@angular/core/primitives/signals'; |
| 10 | +import {createWatch, ReactiveNode, SIGNAL, defaultEquals} from '@angular/core/primitives/signals'; |
11 | 11 |
|
12 | 12 | describe('computed', () => { |
13 | 13 | it('should create computed', () => { |
@@ -201,4 +201,120 @@ describe('computed', () => { |
201 | 201 | ] as ReactiveNode; |
202 | 202 | expect(node.debugName).toBe('computedSignal'); |
203 | 203 | }); |
| 204 | + |
| 205 | + describe('with custom equal', () => { |
| 206 | + it('should cache exceptions thrown by equal', () => { |
| 207 | + const s = signal(0); |
| 208 | + |
| 209 | + let computedRunCount = 0; |
| 210 | + let equalRunCount = 0; |
| 211 | + const c = computed( |
| 212 | + () => { |
| 213 | + computedRunCount++; |
| 214 | + return s(); |
| 215 | + }, |
| 216 | + { |
| 217 | + equal: () => { |
| 218 | + equalRunCount++; |
| 219 | + throw new Error('equal'); |
| 220 | + }, |
| 221 | + }, |
| 222 | + ); |
| 223 | + |
| 224 | + // equal() isn't run for the initial computation. |
| 225 | + expect(c()).toBe(0); |
| 226 | + expect(computedRunCount).toBe(1); |
| 227 | + expect(equalRunCount).toBe(0); |
| 228 | + |
| 229 | + s.set(1); |
| 230 | + |
| 231 | + // Error is thrown by equal(). |
| 232 | + expect(() => c()).toThrowError('equal'); |
| 233 | + expect(computedRunCount).toBe(2); |
| 234 | + expect(equalRunCount).toBe(1); |
| 235 | + |
| 236 | + // Error is cached; c throws again without needing to rerun computation or equal(). |
| 237 | + expect(() => c()).toThrowError('equal'); |
| 238 | + expect(computedRunCount).toBe(2); |
| 239 | + expect(equalRunCount).toBe(1); |
| 240 | + }); |
| 241 | + |
| 242 | + it('should not track signal reads inside equal', () => { |
| 243 | + const value = signal(1); |
| 244 | + const epsilon = signal(0.5); |
| 245 | + |
| 246 | + let innerRunCount = 0; |
| 247 | + let equalRunCount = 0; |
| 248 | + const inner = computed( |
| 249 | + () => { |
| 250 | + innerRunCount++; |
| 251 | + return value(); |
| 252 | + }, |
| 253 | + { |
| 254 | + equal: (a, b) => { |
| 255 | + equalRunCount++; |
| 256 | + return Math.abs(a - b) < epsilon(); |
| 257 | + }, |
| 258 | + }, |
| 259 | + ); |
| 260 | + |
| 261 | + let outerRunCount = 0; |
| 262 | + const outer = computed(() => { |
| 263 | + outerRunCount++; |
| 264 | + return inner(); |
| 265 | + }); |
| 266 | + |
| 267 | + // Everything runs the first time. |
| 268 | + expect(outer()).toBe(1); |
| 269 | + expect(innerRunCount).toBe(1); |
| 270 | + expect(outerRunCount).toBe(1); |
| 271 | + |
| 272 | + // Difference is less than epsilon(). |
| 273 | + value.set(1.2); |
| 274 | + |
| 275 | + // `inner` reruns because `value` was changed, and `equal` is called for the first time. |
| 276 | + expect(outer()).toBe(1); |
| 277 | + expect(innerRunCount).toBe(2); |
| 278 | + expect(equalRunCount).toBe(1); |
| 279 | + // `outer does not rerun because `equal` determined that `inner` had not changed. |
| 280 | + expect(outerRunCount).toBe(1); |
| 281 | + |
| 282 | + // Previous difference is now greater than epsilon(). |
| 283 | + epsilon.set(0.1); |
| 284 | + |
| 285 | + // While changing `epsilon` would change the outcome of the `inner`, we don't rerun it |
| 286 | + // because we intentionally don't track reactive reads in `equal`. |
| 287 | + expect(outer()).toBe(1); |
| 288 | + expect(innerRunCount).toBe(2); |
| 289 | + expect(equalRunCount).toBe(1); |
| 290 | + // Equally important is that the signal read in `equal` doesn't leak into the outer reactive |
| 291 | + // context either. |
| 292 | + expect(outerRunCount).toBe(1); |
| 293 | + }); |
| 294 | + |
| 295 | + it('should recover from exception', () => { |
| 296 | + let shouldThrow = true; |
| 297 | + const source = signal(0); |
| 298 | + const derived = computed(source, { |
| 299 | + equal: (a, b) => { |
| 300 | + if (shouldThrow) { |
| 301 | + throw new Error('equal'); |
| 302 | + } |
| 303 | + return defaultEquals(a, b); |
| 304 | + }, |
| 305 | + }); |
| 306 | + |
| 307 | + // Initial read doesn't throw because it doesn't call `equal`. |
| 308 | + expect(derived()).toBe(0); |
| 309 | + |
| 310 | + // Update `source` to begin throwing. |
| 311 | + source.set(1); |
| 312 | + expect(() => derived()).toThrowError('equal'); |
| 313 | + |
| 314 | + // Stop throwing and update `source` to cause `derived` to recompute. |
| 315 | + shouldThrow = false; |
| 316 | + source.set(2); |
| 317 | + expect(derived()).toBe(2); |
| 318 | + }); |
| 319 | + }); |
204 | 320 | }); |
0 commit comments