Skip to content

Commit 57e9a04

Browse files
authoredFeb 14, 2020
Implement using Proxy (#73)
1 parent e0a8b71 commit 57e9a04

File tree

4 files changed

+154
-46
lines changed

4 files changed

+154
-46
lines changed
 

‎.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@ node_js:
33
- '12'
44
- '10'
55
- '8'
6-
- '6'

‎index.js

+62-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const processFn = (fn, options) => function (...args) {
3+
const processFn = (fn, options, proxy, unwrapped) => function (...args) {
44
const P = options.promiseModule;
55

66
return new P((resolve, reject) => {
@@ -29,10 +29,13 @@ const processFn = (fn, options) => function (...args) {
2929
args.push(resolve);
3030
}
3131

32-
fn.apply(this, args);
32+
const self = this === proxy ? unwrapped : this;
33+
Reflect.apply(fn, self, args);
3334
});
3435
};
3536

37+
const filterCache = new WeakMap();
38+
3639
module.exports = (input, options) => {
3740
options = Object.assign({
3841
exclude: [/.+(Sync|Stream)$/],
@@ -45,24 +48,65 @@ module.exports = (input, options) => {
4548
throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``);
4649
}
4750

48-
const filter = key => {
49-
const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);
50-
return options.include ? options.include.some(match) : !options.exclude.some(match);
51+
const filter = (target, key) => {
52+
let cached = filterCache.get(target);
53+
54+
if (!cached) {
55+
cached = {};
56+
filterCache.set(target, cached);
57+
}
58+
59+
if (key in cached) {
60+
return cached[key];
61+
}
62+
63+
const match = pattern => (typeof pattern === 'string' || typeof key === 'symbol') ? key === pattern : pattern.test(key);
64+
const desc = Reflect.getOwnPropertyDescriptor(target, key);
65+
const writableOrConfigurableOwn = (desc === undefined || desc.writable || desc.configurable);
66+
const included = options.include ? options.include.some(match) : !options.exclude.some(match);
67+
const shouldFilter = included && writableOrConfigurableOwn;
68+
cached[key] = shouldFilter;
69+
return shouldFilter;
5170
};
5271

53-
let ret;
54-
if (objType === 'function') {
55-
ret = function (...args) {
56-
return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args);
57-
};
58-
} else {
59-
ret = Object.create(Object.getPrototypeOf(input));
60-
}
72+
const cache = new WeakMap();
6173

62-
for (const key in input) { // eslint-disable-line guard-for-in
63-
const property = input[key];
64-
ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property;
65-
}
74+
const proxy = new Proxy(input, {
75+
apply(target, thisArg, args) {
76+
const cached = cache.get(target);
77+
78+
if (cached) {
79+
return Reflect.apply(cached, thisArg, args);
80+
}
81+
82+
const pified = options.excludeMain ? target : processFn(target, options, proxy, target);
83+
cache.set(target, pified);
84+
return Reflect.apply(pified, thisArg, args);
85+
},
86+
87+
get(target, key) {
88+
const prop = target[key];
89+
90+
// eslint-disable-next-line no-use-extend-native/no-use-extend-native
91+
if (!filter(target, key) || prop === Function.prototype[key]) {
92+
return prop;
93+
}
94+
95+
const cached = cache.get(prop);
96+
97+
if (cached) {
98+
return cached;
99+
}
100+
101+
if (typeof prop === 'function') {
102+
const pified = processFn(prop, options, proxy, target);
103+
cache.set(prop, pified);
104+
return pified;
105+
}
106+
107+
return prop;
108+
}
109+
});
66110

67-
return ret;
111+
return proxy;
68112
};

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"bluebird"
4444
],
4545
"devDependencies": {
46-
"ava": "^0.25.0",
46+
"ava": "^2.4.0",
4747
"pinkie-promise": "^2.0.0",
4848
"v8-natives": "^1.1.0",
4949
"xo": "^0.23.0"

‎test.js

+91-26
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ test('`errorFirst` option and `multiArgs`', async t => {
176176
})('🦄', '🌈'), ['🦄', '🌈']);
177177
});
178178

179-
test('class support - creates a copy', async t => {
179+
test('class support - does not create a copy', async t => {
180180
const obj = {
181181
x: 'foo',
182182
y(cb) {
@@ -186,28 +186,17 @@ test('class support - creates a copy', async t => {
186186
}
187187
};
188188

189-
const pified = m(obj, {bind: false});
189+
const pified = m(obj);
190190
obj.x = 'bar';
191191

192-
t.is(await pified.y(), 'foo');
193-
t.is(pified.x, 'foo');
192+
t.is(await pified.y(), 'bar');
193+
t.is(pified.x, 'bar');
194194
});
195195

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

200-
const flattened = {};
201-
for (let prot = instance; prot; prot = Object.getPrototypeOf(prot)) {
202-
Object.assign(flattened, prot);
203-
}
204-
205-
const keys = Object.keys(flattened);
206-
keys.sort();
207-
const pKeys = Object.keys(pInstance);
208-
pKeys.sort();
209-
t.deepEqual(keys, pKeys);
210-
211200
t.is(instance.value1, pInstance.value1);
212201
t.is(typeof pInstance.instanceMethod1().then, 'function');
213202
t.is(typeof pInstance.method1().then, 'function');
@@ -236,17 +225,6 @@ test('class support - transforms only members in options.include, copies all', t
236225
include: ['parentMethod1']
237226
});
238227

239-
const flattened = {};
240-
for (let prot = instance; prot; prot = Object.getPrototypeOf(prot)) {
241-
Object.assign(flattened, prot);
242-
}
243-
244-
const keys = Object.keys(flattened);
245-
keys.sort();
246-
const pKeys = Object.keys(pInstance);
247-
pKeys.sort();
248-
t.deepEqual(keys, pKeys);
249-
250228
t.is(typeof pInstance.parentMethod1().then, 'function');
251229
t.not(typeof pInstance.method1(() => {}).then, 'function');
252230
t.not(typeof pInstance.grandparentMethod1(() => {}).then, 'function');
@@ -278,3 +256,90 @@ test('promisify prototype function', async t => {
278256
const instance = new FixtureClass();
279257
t.is(await instance.method2Async(), 72);
280258
});
259+
260+
test('method mutation', async t => {
261+
const obj = {
262+
foo(cb) {
263+
setImmediate(() => cb(null, 'original'));
264+
}
265+
};
266+
const pified = m(obj);
267+
268+
obj.foo = cb => setImmediate(() => cb(null, 'new'));
269+
270+
t.is(await pified.foo(), 'new');
271+
});
272+
273+
test('symbol keys', async t => {
274+
await t.notThrowsAsync(async () => {
275+
const sym = Symbol('sym');
276+
const obj = {[sym]: cb => setImmediate(cb)};
277+
const pified = m(obj);
278+
await pified[sym]();
279+
});
280+
});
281+
282+
// [[Get]] for proxy objects enforces the following invariants: The value
283+
// reported for a property must be the same as the value of the corresponding
284+
// target object property if the target object property is a non-writable,
285+
// non-configurable own data property.
286+
test('non-writable non-configurable property', t => {
287+
const obj = {};
288+
Object.defineProperty(obj, 'prop', {
289+
value: cb => setImmediate(cb),
290+
writable: false,
291+
configurable: false
292+
});
293+
294+
const pified = m(obj);
295+
t.notThrows(() => Reflect.get(pified, 'prop'));
296+
});
297+
298+
test('do not promisify Function.prototype.bind', async t => {
299+
function fn(cb) {
300+
cb(null, this);
301+
}
302+
const target = {};
303+
t.is(await m(fn).bind(target)(), target);
304+
});
305+
306+
test('do not break internal callback usage', async t => {
307+
const obj = {
308+
foo(cb) {
309+
this.bar(4, cb);
310+
},
311+
bar(...args) {
312+
const cb = args.pop();
313+
cb(null, 42);
314+
}
315+
};
316+
t.is(await m(obj).foo(), 42);
317+
});
318+
319+
test('Function.prototype.call', async t => {
320+
function fn(...args) {
321+
const cb = args.pop();
322+
cb(null, args.length);
323+
}
324+
const pified = m(fn);
325+
t.is(await pified.call(), 0);
326+
});
327+
328+
test('Function.prototype.apply', async t => {
329+
function fn(...args) {
330+
const cb = args.pop();
331+
cb(null, args.length);
332+
}
333+
const pified = m(fn);
334+
t.is(await pified.apply(), 0);
335+
});
336+
337+
test('self as member', async t => {
338+
function fn(...args) {
339+
const cb = args.pop();
340+
cb(null, args.length);
341+
}
342+
fn.self = fn;
343+
const pified = m(fn);
344+
t.is(await pified.self(), 0);
345+
});

0 commit comments

Comments
 (0)
Please sign in to comment.