Skip to content

Commit

Permalink
Fix Map and Set iterator methods. (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
DarrenPaulWright committed Jan 16, 2021
1 parent 23f5188 commit 5045523
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 38 deletions.
41 changes: 29 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const {TARGET, UNSUBSCRIBE} = require('./lib/constants');
const isBuiltin = require('./lib/is-builtin');
const path = require('./lib/path');
const isSymbol = require('./lib/is-symbol');
const isIterator = require('./lib/is-iterator');
const wrapIterator = require('./lib/wrap-iterator');
const ignoreProperty = require('./lib/ignore-property');
const Cache = require('./lib/cache');
const SmartClone = require('./lib/smart-clone');
Expand Down Expand Up @@ -53,6 +55,25 @@ const onChange = (object, onChange, options = {}) => {
return value;
};

const prepareValue = (value, target, property, basePath) => {
if (
isBuiltin.withoutMutableMethods(value) ||
property === 'constructor' ||
(isShallow && !SmartClone.isHandledMethod(target, property)) ||
ignoreProperty(cache, options, property) ||
cache.isGetInvariant(target, property) ||
(ignoreDetached && cache.isDetached(target, object))
) {
return value;
}

if (basePath === undefined) {
basePath = cache.getPath(target);
}

return cache.getProxy(value, path.concat(basePath, property), handler, proxyTarget);
};

const handler = {
get(target, property, receiver) {
if (isSymbol(property)) {
Expand All @@ -74,18 +95,7 @@ const onChange = (object, onChange, options = {}) => {
Reflect.get(target, property) :
Reflect.get(target, property, receiver);

if (
isBuiltin.withoutMutableMethods(value) ||
property === 'constructor' ||
(isShallow && !SmartClone.isHandledMethod(target, property)) ||
ignoreProperty(cache, options, property) ||
cache.isGetInvariant(target, property) ||
(ignoreDetached && cache.isDetached(target, object))
) {
return value;
}

return cache.getProxy(value, path.concat(cache.getPath(target), property), handler, proxyTarget);
return prepareValue(value, target, property);
},

set(target, property, value, receiver) {
Expand Down Expand Up @@ -166,6 +176,13 @@ const onChange = (object, onChange, options = {}) => {
}
}

if (
(thisArg instanceof Map || thisArg instanceof Set) &&
isIterator(result)
) {
return wrapIterator(result, target, thisArg, applyPath, prepareValue);
}

return (SmartClone.isHandledType(result) && isHandledMethod) ?
cache.getProxy(result, applyPath, handler, proxyTarget) :
result;
Expand Down
3 changes: 3 additions & 0 deletions lib/is-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = value => typeof value === 'object' && typeof value.next === 'function';
15 changes: 11 additions & 4 deletions lib/smart-clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ const IMMUTABLE_ARRAY_METHODS = new Set([

const IMMUTABLE_SET_METHODS = new Set([
'has',
'toString',
'keys'
'toString'
]);

const IMMUTABLE_MAP_METHODS = new Set([...IMMUTABLE_SET_METHODS].concat(['get']));
Expand All @@ -87,6 +86,12 @@ const SHALLOW_MUTABLE_SET_METHODS = {
delete: shallowEqualSets
};

const COLLECTION_ITERATOR_METHODS = [
'keys',
'values',
'entries'
];

const SHALLOW_MUTABLE_MAP_METHODS = {
set: shallowEqualMaps,
clear: shallowEqualMaps,
Expand All @@ -98,10 +103,12 @@ const HANDLED_ARRAY_METHODS = new Set([...IMMUTABLE_OBJECT_METHODS]
.concat(Object.keys(SHALLOW_MUTABLE_ARRAY_METHODS)));

const HANDLED_SET_METHODS = new Set([...IMMUTABLE_SET_METHODS]
.concat(Object.keys(SHALLOW_MUTABLE_SET_METHODS)));
.concat(Object.keys(SHALLOW_MUTABLE_SET_METHODS))
.concat(COLLECTION_ITERATOR_METHODS));

const HANDLED_MAP_METHODS = new Set([...IMMUTABLE_MAP_METHODS]
.concat(Object.keys(SHALLOW_MUTABLE_MAP_METHODS)));
.concat(Object.keys(SHALLOW_MUTABLE_MAP_METHODS))
.concat(COLLECTION_ITERATOR_METHODS));

class Clone {
constructor(value, path, argumentsList) {
Expand Down
65 changes: 65 additions & 0 deletions lib/wrap-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const {TARGET} = require('./constants');

// eslint-disable-next-line max-params
module.exports = (iterator, target, thisArg, applyPath, prepareValue) => {
const originalNext = iterator.next;

if (target.name === 'entries') {
iterator.next = function () {
const result = originalNext.call(this);

if (result.done === false) {
result.value[0] = prepareValue(
result.value[0],
target,
result.value[0],
applyPath
);
result.value[1] = prepareValue(
result.value[1],
target,
result.value[0],
applyPath
);
}

return result;
};
} else if (target.name === 'values') {
const keyIterator = thisArg[TARGET].keys();

iterator.next = function () {
const result = originalNext.call(this);

if (result.done === false) {
result.value = prepareValue(
result.value,
target,
keyIterator.next().value,
applyPath
);
}

return result;
};
} else {
iterator.next = function () {
const result = originalNext.call(this);

if (result.done === false) {
result.value = prepareValue(
result.value,
target,
result.value,
applyPath
);
}

return result;
};
}

return iterator;
};
161 changes: 139 additions & 22 deletions tests/on-change.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1251,39 +1251,52 @@ test('should only execute once if map is called within callback', t => {
t.is(count, 1);
});

test('should return an array iterator when array.keys is called', t => {
let count = 0;
let callbackCount = 0;
const array = ['a', 'b', 'c'];
test('should return an array iterator when array[Symbol.iterator] is called', t => {
const array = [{a: 1}, {a: 2}];

testHelper(t, array, {}, (proxy, verify) => {
for (const entry of proxy[Symbol.iterator]()) {
entry.a++;
}

const proxy = onChange(array, () => {
callbackCount++;
verify(2, proxy, '1.a', 3, 2);
});
});

for (const index of proxy.keys()) {
t.is(count++, index);
}
test('should return an array iterator when array.keys is called', t => {
const array = [{a: 1}, {a: 2}];

t.is(callbackCount, 0);
testHelper(t, array, {}, (proxy, verify) => {
for (const key of proxy.keys()) {
proxy[key].a++;
}

verify(2, proxy, '1.a', 3, 2);
});
});

test('should return an array iterator when array.entries is called', t => {
let count = 0;
let callbackCount = 0;
const array = [{a: 0}, {a: 1}, {a: 2}];
const array = [{a: 1}, {a: 2}];

const proxy = onChange(array, () => {
callbackCount++;
testHelper(t, array, {}, (proxy, verify) => {
for (const entry of proxy.entries()) {
entry[1].a++;
}

verify(2, proxy, '1.a', 3, 2);
});
});

for (const [index, element] of proxy.entries()) {
t.is(count++, index);
t.is(callbackCount, element.a);
element.a++;
t.is(callbackCount, element.a);
}
test('should return an array iterator when array.values is called', t => {
const array = [{a: 1}, {a: 2}];

testHelper(t, array, {}, (proxy, verify) => {
for (const entry of proxy.values()) {
entry.a++;
}

t.is(callbackCount, 3);
verify(2, proxy, '1.a', 3, 2);
});
});

test('should handle shallow changes to Sets', t => {
Expand Down Expand Up @@ -1316,6 +1329,54 @@ test('should handle shallow changes to Sets', t => {
});
});

test('should return an iterator when Set[Symbol.iterator] is called', t => {
const set = new Set([{a: 1}, {a: 2}]);

testHelper(t, set, {pathAsArray: true}, (proxy, verify) => {
for (const entry of proxy[Symbol.iterator]()) {
entry.a++;
}

verify(2, proxy, [{a: 3}, 'a'], 3, 2);
});
});

test('should return an iterator when Set.keys is called', t => {
const set = new Set([{a: 1}, {a: 2}]);

testHelper(t, set, {pathAsArray: true}, (proxy, verify) => {
for (const key of proxy.keys()) {
key.a++;
}

verify(2, proxy, [{a: 3}, 'a'], 3, 2);
});
});

test('should return an iterator when Set.entries is called', t => {
const set = new Set([{a: 1}, {a: 2}]);

testHelper(t, set, {pathAsArray: true}, (proxy, verify) => {
for (const entry of proxy.entries()) {
entry[1].a++;
}

verify(2, proxy, [{a: 3}, 'a'], 3, 2);
});
});

test('should return an iterator when Set.values is called', t => {
const set = new Set([{a: 1}, {a: 2}]);

testHelper(t, set, {pathAsArray: true}, (proxy, verify) => {
for (const entry of proxy.values()) {
entry.a++;
}

verify(2, proxy, [{a: 3}, 'a'], 3, 2);
});
});

test('should handle shallow changes to WeakSets', t => {
const object = {a: 0};
const set = new WeakSet();
Expand All @@ -1337,6 +1398,62 @@ test('should handle shallow changes to WeakSets', t => {
});
});

test('should return an iterator when Map[Symbol.iterator] is called', t => {
const object = {a: 1};
const map = new Map();
map.set(object, 1);

testHelper(t, map, {pathAsArray: true}, (proxy, verify) => {
for (const entry of proxy[Symbol.iterator]()) {
entry[0].a++;
}

verify(1, proxy, [object, 'a'], 2, 1);
});
});

test('should return an iterator when Map.keys is called', t => {
const object = {a: 1};
const map = new Map();
map.set(object, 1);

testHelper(t, map, {pathAsArray: true}, (proxy, verify) => {
for (const key of proxy.keys()) {
key.a++;
}

verify(1, proxy, [object, 'a'], 2, 1);
});
});

test('should return an iterator when Map.entries is called', t => {
const object = {};
const map = new Map();
map.set(object, {y: 1});

testHelper(t, map, {pathAsArray: true}, (proxy, verify) => {
for (const entry of proxy.entries()) {
entry[1].y++;
}

verify(1, proxy, [object, 'y'], 2, 1);
});
});

test('should return an iterator when Map.values is called', t => {
const object = {};
const map = new Map();
map.set(object, {y: 1});

testHelper(t, map, {pathAsArray: true}, (proxy, verify) => {
for (const value of proxy.values()) {
value.y++;
}

verify(1, proxy, [object, 'y'], 2, 1);
});
});

test('should handle shallow changes to Maps', t => {
const object = {a: 0};
const map = new Map();
Expand Down

0 comments on commit 5045523

Please sign in to comment.