Proxies

When you’re making a call to Node Edge, one of two things can happen:

  • Either the return value can be serialized into JSON, in which case you get a copy of the Node value that has been serialized through JSON (this it’s not “connected” to Node anymore)

  • Or the return value cannot be serialized into JSON, in which case you get a proxy object that will let you call Node functions from Python.

As you might know, the typing of objects in JS is a bit ambiguous and does not always map super well to Pythonic concepts. That’s why we’ve got three different proxies:

  • JavaScriptProxy — Is a proxy for a “class instance” or a “module” or anything that will generally have methods you can call and stuff like this.

  • JavaScriptArrayProxy — Arrays are a special type of objects in JS and they can be detected so if we encounter one that’s the proxy you’ll be getting

  • JavaScriptMappingProxy — This one will treat the object as a mapping (dict-like) and will let you access items on it.

JavaScriptProxy

This is the most common proxy you’ll be getting. Basically most of the calls that you make that don’t end up in JSON will be this.

from node_edge import NodeEngine

with NodeEngine({}) as ne:
    axios = ne.import_from("axios")

What you got here is a JavaScriptProxy. You can either do axios.get or axios["get"], both will be resolved to a pointer to the get method.

However, it’s not a true mapping. Typically if it did implement the Mapping interface, the get() method would get an item from the dictionary instead of calling the get method on the object.

Other than that you can do all the regular attribute operations:

axios.foo = 42
del axios.foo

This will impact the “remote” object directly.

JavaScriptArrayProxy

If the returned value is an array, we detect it and return a JavaScriptArrayProxy which implements the MutableSequence interface.

from node_edge import NodeEngine

with NodeEngine({}) as ne:
    arr = ne.eval("['foo', () => 42]")
    print(len(arr))  # 2

    arr.append("bar")
    print(len(arr))  # 3

    del arr[1]
    print(len(arr))  # 2

Note — Here we put a function in the array in order to make it non-JSON-serializable and thus trigger the return of a proxy.

JavaScriptMappingProxy

Most of the time if you’ve got a proxy, it’s because the object is not JSON-serializable. However, if it’s a mapping, we can still return a proxy that will implement the MutableMapping interface.

This is done by transforming the a JavaScriptProxy into a JavaScriptMappingProxy using the as_mapping() method.

from node_edge import NodeEngine, as_mapping

with NodeEngine({}) as ne:
    axios = ne.import_from("axios")
    resp = axios.get("https://httpbin.org/robots.txt")
    headers = as_mapping(resp.headers)
    print({**headers})

In the cas above, the headers from Axios are stored in a special class that won’t be serialized into JSON but can be converted into a mapping. The print({**headers}) statement serves as a demonstration that this mapping can then be converted into a regular dictionary and printed.

Calls

Something that you’ve seen so far but have not been explained is how function calls work.

Basically, the JavaScriptProxy implements the __call__ method (with only *args, not **kwargs). When you call it, it will forward the call to the JS side, wait for the promise to resolve if required and then return the result.

The arguments you pass can either be:

  • Something that can be serialized into JSON

  • Any of the proxies

The proxy can be nested deep inside the JSON structure, it will be detected and converted back into its JS counterpart once in Node.

For example, you can imagine something like this:

from node_edge import NodeEngine

with NodeEngine({}) as ne:
    ne.eval(
        """
        function foo() {
            return 'foo';
        }

        function bar() {
            return 'bar';
        }

        function execFromMap(functions, name) {
            return functions[name]();
        }
        """
    )

    foo = ne.eval("foo")
    bar = ne.eval("bar")
    exec_from_map = ne.eval("execFromMap")

    print(exec_from_map({"foo": foo, "bar": bar}, "foo"))  # foo

For now you cannot give Python callbacks as arguments, as it’s not coded yet and coding it would probably be a bit confusing thread-wise. My advice is to create a Node function (using eval() like above) that will deal with all the JS-side logic and return a Promise that will resolve with the output you’re expecting. Since function calls automatically await the output, you’ll simply get your output like that.

If you want a long-lived part of your code to be executed in Node and communicating with Python, that’s clearly out of scope for now.