Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Illegal invocation error using ES6 Proxy and node.js

I can not figure out why the following code does not work:

var os = new Proxy(require('os'), {});
console.log( os.cpus() ); // TypeError: Illegal invocation

whereas

var os = require('os');
console.log(Reflect.apply(os.cpus, os, []));

or

var os = new Proxy(require('os'), {});
console.log( os.platform() );

works as expected.

like image 917
Franck Freiburger Avatar asked Feb 27 '17 21:02

Franck Freiburger


People also ask

What is illegal invocation in JS?

An "illegal invocation" error is thrown when calling a function whose this keyword doesn't refer to the object where it originally did. In other words, the original "context" of the function is lost. Chromium browsers call this error an "illegal invocation."

What is ES6 proxy?

ES6 implements intercession form of meta programming using Proxies. Similar to ReflectAPI, the Proxy API is another way of implementing meta programming in ES6. The Proxy object is used to define custom behavior for fundamental operations. A proxy object performs some operations on behalf of the real object.

What does a proxy do to the target object Ecmascript?

A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object's property. The original object the proxy will virtualize.


2 Answers

Having just skim read the source for the os package in the Node repo, it appears that the cpus() is exported from binding.getCPUs which is a C hook in the Node runtime environment.

cpus() therefore has the binding object as a function context, which is then lost through the proxy, giving you the IllegalInvocation error because there is no context to the function when you call it — although I'm hazy on the details.

platform() on the other hand is exported as function () { return process.platform; }, and hence it's just a function that returns an object, and doesn't need to be run under a specific context because Node function contexts will have the process variable specified by default (unless it has been overridden).

The following behaviour shows that applying the os as a context to the cpus function will work — proxies on function objects evidently lose the function context when calling properties.

const os = require('os');
const proxy = new Proxy(os, {});  // proxy of object, functions called get proxy context rather than os context
const cpus = new Proxy(os.cpus, {});  // proxy of function, still has os context

console.log(os.cpus());  // works (duh)
console.log(cpus());     // works
console.log(proxy.cpus.apply(os, []));  // works
console.log(proxy.cpus());  // fails with IllegalInvocation

Note: If someone can clear up the details on the JS function context for an answer I'd love to read it too.

like image 86
Liam Gray Avatar answered Nov 15 '22 00:11

Liam Gray


How about composition:

const os = require('os');
const proxy = new Proxy(os, {});
Object.getOwnPropertyNames(os).forEach(k => {
    var v = os[k];
    if(typeof v === "function") proxy[k] = v.bind(os);
});

//the `!!` because I don't want the actual print
//only a `true` or an `Error`
console.log(!!os.cpus());
console.log(!!proxy.cpus());
console.log(!!proxy.cpus.apply(proxy, []));

and all this as a utility function to "replace" new Proxy(), where handler.bindTargetFunctions can be

  • either an array of keyNames to be bound (so you can be specific)
  • or any truthy or falsy value to determine wether all functions on the target should be bound

the code:

function proxy(target, handler){
    const _proxy = new Proxy(target, handler);
    if(handler.bindTargetFunctions){
        let bindTargetFunctions = handler.bindTargetFunctions;
        if(!Array.isArray(bindTargetFunctions)){
            bindTargetFunctions = Object.getOwnPropertyNames(target)
                .filter(key => typeof target[key] === "function");
        }
        bindTargetFunctions.forEach(key => {
            _proxy[key] = target[key].bind(target);
        });
    }
    return _proxy;
}

const os = proxy(require('os'), { bindTargetFunctions: true });
//or
//const os = proxy(require('os'), { bindTargetFunctions: ["cpus"] });

console.log(os.cpus());

Edit:

Currently I try to bind functions directly in my get handler (see github.com/FranckFreiburger/module-invalidate/blob/master/…)‌​, the drawback of my solution is that each access to a function returns a new binding.

I entioned caching in the comments. This is how this cache could look like:

function createProxy(mod){
    var cache = Object.create(null);

    return new Proxy(function(){}, {
        get(target, property, receiver) {
            var val = Reflect.get(mod._exports, property, receiver);
            if(typeof val === "function"){
                if(!(property in cache) || cache[property].original !== val){
                    cache[property] = {
                        original: val,
                        bound: bal.bind(mod._exports)
                    }
                }
                val = cache[property].bound;
            }else if(property in cache){
                delete cache[property];
            }

            return val;
        }
    });
}

And No, I don't consider this cache a regular object. Not because it inherits from null, but because logically, to me this is a dictionary/map. And I don't know any reason why you would ever extend or proxy a particular dictionary.

like image 36
Thomas Avatar answered Nov 15 '22 00:11

Thomas