Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement using Proxy #73

Merged
merged 12 commits into from Feb 14, 2020
73 changes: 56 additions & 17 deletions index.js
Expand Up @@ -29,10 +29,12 @@ const processFn = (fn, options) => function (...args) {
args.push(resolve);
}

fn.apply(this, args);
Reflect.apply(fn, this, args);
});
};

const filterCache = new WeakMap();

module.exports = (input, options) => {
options = Object.assign({
exclude: [/.+(Sync|Stream)$/],
Expand All @@ -45,24 +47,61 @@ module.exports = (input, options) => {
throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``);
}

const filter = key => {
const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);
return options.include ? options.include.some(match) : !options.exclude.some(match);
const filter = (target, key) => {
let cached = filterCache.get(target);

if (!cached) {
cached = {};
filterCache.set(target, cached);
}

if (key in cached) {
return cached[key];
}

const match = pattern => (typeof pattern === 'string' || typeof key === 'symbol') ? key === pattern : pattern.test(key);
const shouldFilter = options.include ? options.include.some(match) : !options.exclude.some(match);
cached[key] = shouldFilter;
return shouldFilter;
};

let ret;
if (objType === 'function') {
ret = function (...args) {
return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args);
};
} else {
ret = Object.create(Object.getPrototypeOf(input));
}
const cache = new WeakMap();

for (const key in input) { // eslint-disable-line guard-for-in
const property = input[key];
ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property;
}
const handler = {
apply(target, thisArg, args) {
const cached = cache.get(target);

if (cached) {
return Reflect.apply(cached, thisArg, args);
}

const pified = options.excludeMain ? target : processFn(target, options);
cache.set(target, pified);
return Reflect.apply(pified, thisArg, args);
},

get(target, key) {
const prop = target[key];

if (!filter(target, key)) {
return prop;
}

const cached = cache.get(prop);

if (cached) {
return cached;
}

if (typeof prop === 'function') {
const pified = processFn(prop, options);
cache.set(prop, pified);
return pified;
}

return prop;
}
};

return ret;
return new Proxy(input, handler);
};
51 changes: 25 additions & 26 deletions test.js
Expand Up @@ -176,7 +176,7 @@ test('`errorFirst` option and `multiArgs`', async t => {
})('🦄', '🌈'), ['🦄', '🌈']);
});

test('class support - creates a copy', async t => {
test('class support - does not create a copy', async t => {
const obj = {
x: 'foo',
y(cb) {
Expand All @@ -186,28 +186,17 @@ test('class support - creates a copy', async t => {
}
};

const pified = m(obj, {bind: false});
const pified = m(obj);
obj.x = 'bar';

t.is(await pified.y(), 'foo');
t.is(pified.x, 'foo');
t.is(await pified.y(), 'bar');
t.is(pified.x, 'bar');
});

test('class support — transforms inherited methods', t => {
const instance = new FixtureClass();
const pInstance = m(instance);

const flattened = {};
for (let prot = instance; prot; prot = Object.getPrototypeOf(prot)) {
Object.assign(flattened, prot);
}

const keys = Object.keys(flattened);
keys.sort();
const pKeys = Object.keys(pInstance);
pKeys.sort();
t.deepEqual(keys, pKeys);

t.is(instance.value1, pInstance.value1);
t.is(typeof pInstance.instanceMethod1().then, 'function');
t.is(typeof pInstance.method1().then, 'function');
Expand Down Expand Up @@ -236,17 +225,6 @@ test('class support - transforms only members in options.include, copies all', t
include: ['parentMethod1']
});

const flattened = {};
for (let prot = instance; prot; prot = Object.getPrototypeOf(prot)) {
Object.assign(flattened, prot);
}

const keys = Object.keys(flattened);
keys.sort();
const pKeys = Object.keys(pInstance);
pKeys.sort();
t.deepEqual(keys, pKeys);

t.is(typeof pInstance.parentMethod1().then, 'function');
t.not(typeof pInstance.method1(() => {}).then, 'function');
t.not(typeof pInstance.grandparentMethod1(() => {}).then, 'function');
Expand Down Expand Up @@ -278,3 +256,24 @@ test('promisify prototype function', async t => {
const instance = new FixtureClass();
t.is(await instance.method2Async(), 72);
});

test('method mutation', async t => {
const obj = {
foo(cb) {
setImmediate(() => cb(null, 'original'));
}
};
const pified = m(obj);

obj.foo = cb => setImmediate(() => cb(null, 'new'));

t.is(await pified.foo(), 'new');
});

test('symbol keys', async t => {
const sym = Symbol('sym');
const obj = {[sym]: cb => setImmediate(cb)};
const pified = m(obj);
await pified[sym]();
t.pass();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use t.notThrows instead.

});