Skip to content

Commit 5a9adac

Browse files
authored
refactor: simplify cache usage for object cycles (#77)
* add test with object cycles * simplify cache usage * should not reach there
1 parent 1c0f138 commit 5a9adac

File tree

2 files changed

+75
-51
lines changed

2 files changed

+75
-51
lines changed

src/index.ts

+29-51
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ const IS_TARGET_COPIED_PROPERTY = 'f';
1010
const PROXY_PROPERTY = 'p';
1111
const PROXY_CACHE_PROPERTY = 'c';
1212
const TARGET_CACHE_PROPERTY = 't';
13-
const NEXT_OBJECT_PROPERTY = 'n';
14-
const CHANGED_PROPERTY = 'g';
1513
const HAS_KEY_PROPERTY = 'h';
1614
const ALL_OWN_KEYS_PROPERTY = 'w';
1715
const HAS_OWN_KEY_PROPERTY = 'o';
@@ -252,14 +250,6 @@ const isAllOwnKeysChanged = (prevObj: object, nextObj: object) => {
252250
);
253251
};
254252

255-
type ChangedCache = WeakMap<
256-
object,
257-
{
258-
[NEXT_OBJECT_PROPERTY]: object;
259-
[CHANGED_PROPERTY]: boolean;
260-
}
261-
>;
262-
263253
/**
264254
* Compare changes on objects.
265255
*
@@ -299,7 +289,7 @@ export const isChanged = (
299289
prevObj: unknown,
300290
nextObj: unknown,
301291
affected: WeakMap<object, unknown>,
302-
cache?: WeakMap<object, unknown>,
292+
cache?: WeakMap<object, unknown>, // for object with cycles
303293
isEqual: (a: unknown, b: unknown) => boolean = Object.is,
304294
): boolean => {
305295
if (isEqual(prevObj, nextObj)) {
@@ -309,53 +299,41 @@ export const isChanged = (
309299
const used = (affected as Affected).get(getOriginalObject(prevObj));
310300
if (!used) return true;
311301
if (cache) {
312-
const hit = (cache as ChangedCache).get(prevObj);
313-
if (hit && hit[NEXT_OBJECT_PROPERTY] === nextObj) {
314-
return hit[CHANGED_PROPERTY];
302+
const hit = cache.get(prevObj);
303+
if (hit === nextObj) {
304+
return false;
315305
}
316306
// for object with cycles
317-
(cache as ChangedCache).set(prevObj, {
318-
[NEXT_OBJECT_PROPERTY]: nextObj,
319-
[CHANGED_PROPERTY]: false,
320-
});
307+
cache.set(prevObj, nextObj);
321308
}
322309
let changed: boolean | null = null;
323-
try {
324-
for (const key of used[HAS_KEY_PROPERTY] || []) {
325-
changed = Reflect.has(prevObj, key) !== Reflect.has(nextObj, key);
326-
if (changed) return changed;
327-
}
328-
if (used[ALL_OWN_KEYS_PROPERTY] === true) {
329-
changed = isAllOwnKeysChanged(prevObj, nextObj);
330-
if (changed) return changed;
331-
} else {
332-
for (const key of used[HAS_OWN_KEY_PROPERTY] || []) {
333-
const hasPrev = !!Reflect.getOwnPropertyDescriptor(prevObj, key);
334-
const hasNext = !!Reflect.getOwnPropertyDescriptor(nextObj, key);
335-
changed = hasPrev !== hasNext;
336-
if (changed) return changed;
337-
}
338-
}
339-
for (const key of used[KEYS_PROPERTY] || []) {
340-
changed = isChanged(
341-
(prevObj as any)[key],
342-
(nextObj as any)[key],
343-
affected,
344-
cache,
345-
isEqual,
346-
);
310+
for (const key of used[HAS_KEY_PROPERTY] || []) {
311+
changed = Reflect.has(prevObj, key) !== Reflect.has(nextObj, key);
312+
if (changed) return changed;
313+
}
314+
if (used[ALL_OWN_KEYS_PROPERTY] === true) {
315+
changed = isAllOwnKeysChanged(prevObj, nextObj);
316+
if (changed) return changed;
317+
} else {
318+
for (const key of used[HAS_OWN_KEY_PROPERTY] || []) {
319+
const hasPrev = !!Reflect.getOwnPropertyDescriptor(prevObj, key);
320+
const hasNext = !!Reflect.getOwnPropertyDescriptor(nextObj, key);
321+
changed = hasPrev !== hasNext;
347322
if (changed) return changed;
348323
}
349-
if (changed === null) changed = true;
350-
return changed;
351-
} finally {
352-
if (cache) {
353-
cache.set(prevObj, {
354-
[NEXT_OBJECT_PROPERTY]: nextObj,
355-
[CHANGED_PROPERTY]: changed,
356-
});
357-
}
358324
}
325+
for (const key of used[KEYS_PROPERTY] || []) {
326+
changed = isChanged(
327+
(prevObj as any)[key],
328+
(nextObj as any)[key],
329+
affected,
330+
cache,
331+
isEqual,
332+
);
333+
if (changed) return changed;
334+
}
335+
if (changed === null) throw new Error('invalid used');
336+
return changed;
359337
};
360338

361339
// explicitly track object with memo

tests/17_cycles.spec.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createProxy, isChanged } from 'proxy-compare';
3+
4+
const noop = (_arg: unknown) => {
5+
// do nothing
6+
};
7+
8+
describe('object with cycles', () => {
9+
interface S {
10+
a: string;
11+
s?: S;
12+
}
13+
14+
it('without cache', () => {
15+
const s1: S = { a: 'a' };
16+
s1.s = s1;
17+
const s2: S = { a: 'a' };
18+
s2.s = s2;
19+
const a1 = new WeakMap();
20+
const p1 = createProxy(s1, a1);
21+
noop(p1.s?.a);
22+
expect(() => isChanged(s1, s2, a1)).toThrow();
23+
});
24+
25+
it('with cache', () => {
26+
const s1: S = { a: 'a' };
27+
s1.s = s1;
28+
const s2: S = { a: 'a' };
29+
s2.s = s2;
30+
const a1 = new WeakMap();
31+
const p1 = createProxy(s1, a1);
32+
noop(p1.s?.a);
33+
expect(isChanged(s1, s2, a1, new WeakMap())).toBe(false);
34+
});
35+
36+
it('with cache with a change', () => {
37+
const s1: S = { a: 'a' };
38+
s1.s = s1;
39+
const s2: S = { a: 'aa' };
40+
s2.s = s2;
41+
const a1 = new WeakMap();
42+
const p1 = createProxy(s1, a1);
43+
noop(p1.s?.a);
44+
expect(isChanged(s1, s2, a1, new WeakMap())).toBe(true);
45+
});
46+
});

0 commit comments

Comments
 (0)