From 57e9a0442e9e1224e73b844f1ed89b9bdbd10ec6 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 14 Feb 2020 12:33:04 -0300 Subject: [PATCH] Implement using Proxy (#73) --- .travis.yml | 1 - index.js | 80 +++++++++++++++++++++++++++-------- package.json | 2 +- test.js | 117 +++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 154 insertions(+), 46 deletions(-) diff --git a/.travis.yml b/.travis.yml index e155464..f98fed0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,3 @@ node_js: - '12' - '10' - '8' - - '6' diff --git a/index.js b/index.js index df56221..ccbee49 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ 'use strict'; -const processFn = (fn, options) => function (...args) { +const processFn = (fn, options, proxy, unwrapped) => function (...args) { const P = options.promiseModule; return new P((resolve, reject) => { @@ -29,10 +29,13 @@ const processFn = (fn, options) => function (...args) { args.push(resolve); } - fn.apply(this, args); + const self = this === proxy ? unwrapped : this; + Reflect.apply(fn, self, args); }); }; +const filterCache = new WeakMap(); + module.exports = (input, options) => { options = Object.assign({ exclude: [/.+(Sync|Stream)$/], @@ -45,24 +48,65 @@ 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 desc = Reflect.getOwnPropertyDescriptor(target, key); + const writableOrConfigurableOwn = (desc === undefined || desc.writable || desc.configurable); + const included = options.include ? options.include.some(match) : !options.exclude.some(match); + const shouldFilter = included && writableOrConfigurableOwn; + 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 proxy = new Proxy(input, { + 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, proxy, target); + cache.set(target, pified); + return Reflect.apply(pified, thisArg, args); + }, + + get(target, key) { + const prop = target[key]; + + // eslint-disable-next-line no-use-extend-native/no-use-extend-native + if (!filter(target, key) || prop === Function.prototype[key]) { + return prop; + } + + const cached = cache.get(prop); + + if (cached) { + return cached; + } + + if (typeof prop === 'function') { + const pified = processFn(prop, options, proxy, target); + cache.set(prop, pified); + return pified; + } + + return prop; + } + }); - return ret; + return proxy; }; diff --git a/package.json b/package.json index a5468cd..68c09bd 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "bluebird" ], "devDependencies": { - "ava": "^0.25.0", + "ava": "^2.4.0", "pinkie-promise": "^2.0.0", "v8-natives": "^1.1.0", "xo": "^0.23.0" diff --git a/test.js b/test.js index 8f89d0d..23aad44 100644 --- a/test.js +++ b/test.js @@ -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) { @@ -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'); @@ -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'); @@ -278,3 +256,90 @@ 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 => { + await t.notThrowsAsync(async () => { + const sym = Symbol('sym'); + const obj = {[sym]: cb => setImmediate(cb)}; + const pified = m(obj); + await pified[sym](); + }); +}); + +// [[Get]] for proxy objects enforces the following invariants: The value +// reported for a property must be the same as the value of the corresponding +// target object property if the target object property is a non-writable, +// non-configurable own data property. +test('non-writable non-configurable property', t => { + const obj = {}; + Object.defineProperty(obj, 'prop', { + value: cb => setImmediate(cb), + writable: false, + configurable: false + }); + + const pified = m(obj); + t.notThrows(() => Reflect.get(pified, 'prop')); +}); + +test('do not promisify Function.prototype.bind', async t => { + function fn(cb) { + cb(null, this); + } + const target = {}; + t.is(await m(fn).bind(target)(), target); +}); + +test('do not break internal callback usage', async t => { + const obj = { + foo(cb) { + this.bar(4, cb); + }, + bar(...args) { + const cb = args.pop(); + cb(null, 42); + } + }; + t.is(await m(obj).foo(), 42); +}); + +test('Function.prototype.call', async t => { + function fn(...args) { + const cb = args.pop(); + cb(null, args.length); + } + const pified = m(fn); + t.is(await pified.call(), 0); +}); + +test('Function.prototype.apply', async t => { + function fn(...args) { + const cb = args.pop(); + cb(null, args.length); + } + const pified = m(fn); + t.is(await pified.apply(), 0); +}); + +test('self as member', async t => { + function fn(...args) { + const cb = args.pop(); + cb(null, args.length); + } + fn.self = fn; + const pified = m(fn); + t.is(await pified.self(), 0); +});