Proxy Reflect.has() call causes infinite recursion?

# #!/JoePea (4 years ago)

I was trying to implement "multiple inheritance" in the following code (jsfiddle), but it gives a max call stack (infinite recursion) error.

However, the infinite recursion does not execute any of my console.log statements repeatedly like I'd expect, so it seems that the infinite recursion is happening inside the JS engine?

What's going on?

Here's the code for reference, see "PROXY INFINITE RECURSION" comments for the site where the problem is happening (as far as I can tell in devtools):

"use strict";

function multiple(...classes) {
    if (classes.length === 0)
        return Object;
    if (classes.length === 1) {
        const result = classes[0];
        return (typeof result === 'function' ? result : Object);
    }

    const FirstClass = classes.shift();
    const __instances__ = new WeakMap();
    const getInstances = (inst) => {
        let result = __instances__.get(inst);
        if (!result)
            __instances__.set(inst, (result = []));
        return result;
    };

    class MultiClass extends FirstClass {
        constructor(...args) {
            super(...args);

            const protoBeforeMultiClassProto =
findPrototypeBeforeMultiClassPrototype(this, MultiClass.prototype);
            if (protoBeforeMultiClassProto)
                Object.setPrototypeOf(protoBeforeMultiClassProto,
newMultiClassPrototype);

            const instances = getInstances(this);

            for (const Ctor of classes)
                instances.push(new Ctor(...args));
        }
    }

    let count = 0;
    const newMultiClassPrototype = new Proxy({
        __proto__: MultiClass.prototype,
    }, {
        get(target, key, self) {
            if (count++ < 500) console.log(' --------------- get', key);

            if (Reflect.has(MultiClass.prototype, key))
                return Reflect.get(MultiClass.prototype, key, self);

            for (const instance of getInstances(self))
                if (Reflect.has(instance, key))
                    return Reflect.get(instance, key, self);

            return undefined;
        },

        set(target, key, value, self) {
            console.log(' ----- set1', key, value);

            // PROXY INFINITE RECURSION HERE:
            console.log('hmmmmmmmmmmmmmmmm?',
Reflect.has(MultiClass.prototype, key));

            console.log(' ----- set1.5', key, value);

            if (Reflect.has(MultiClass.prototype, key)) {
                return Reflect.set(target, key, value, self);
            }

            const instances = getInstances(self);

            for (const instance of instances) {
                if (Reflect.has(instance, key)) {
                    return Reflect.set(instance, key, value, self);
                }
            }

            return Reflect.set(target, key, value, self);
        },
    });

    return MultiClass;
}

function findPrototypeBeforeMultiClassPrototype(obj, multiClassPrototype) {
    let previous = obj;
    let current = Object.getPrototypeOf(obj);
    while (current) {
        // debugger
        if (current === multiClassPrototype)
            return previous;
        previous = current;
        current = Object.getPrototypeOf(current);
    }
    return null;
}

async function test() {
    await new Promise(r => setTimeout(r, 3000));
    console.log('-------------------------------------');
    const R1 = multiple();
    const r1 = new R1();
    console.log(Object.keys(r1));
    console.log('-------------------------------------');
    class Foo {
        constructor() {
            this.f = false;
        }
    }
    const R2 = multiple(Foo);
    const r2 = new R2();
    console.log(r2.hasOwnProperty);
    console.log('f', r2.f);
    console.log('-------------------------------------');
    class Bar {
        constructor() {
            this.b = 'asf';
        }
    }
    const R3 = multiple(Foo, Bar);
    const r3 = new R3();
    console.log(r3.hasOwnProperty);
    console.log('f', r3.f);
    console.log('b', r3.b);
    console.log('-------------------------------------');
    class Baz {
        constructor() {
            this.z = 1;
        }
    }
    const R4 = multiple(Foo, Bar, Baz);
    const r4 = new R4();
    console.log(r4.hasOwnProperty);
    console.log('f', r4.f);
    console.log('b', r4.b);
    console.log('z', r4.z);
    class One {
        constructor(arg) {
            this.one = 1;
            console.log('One constructor');
            // this.one = arg
        }
        foo() {
            console.log('foo', this.one);
        }
        setVar() {
            this.var = 'bright';
        }
    }
    class Two {
        constructor(arg) {
            this.two = 2;
            console.log('Two constructor');
            // this.two = arg
        }
        bar() {
            console.log('bar', this.two);
        }
        readVar() {
            console.log(this.var); // should be "bright"
        }
    }
    class Three extends Two {
        constructor(arg1, arg2) {
            super(arg1);
            this.three = 3;
            console.log('Three constructor');
            // this.three = arg2
        }
        baz() {
            console.log(' - baz: call super.bar');
            super.bar();
            console.log('baz', this.three, this.two);
        }
    }
    class FooBar extends multiple(Three, One) {
        constructor(...args) {
            super();
            // call each constructor. We can pas specific args to each
constructor if we like.
            //
            // XXX The following is not allowed with ES6 classes,
class constructors are not callable. :[ How to solve?
            // One.call(this, ...args)
            // Three.call(this, ...args)
            //
            // XXX Solved with the callSuperConstructor helper.
            // ;(this as any).callSuperConstructor(One, args[0])
            // ;(this as any).callSuperConstructor(Three, args[1], args[2])
        }
        yeah() {
            console.log(' -- yeah', this.one, this.two, this.three);
            super.baz();
            super.foo();
        }
    }
    let f = new FooBar(1, 2, 3);
    // this shows that the modifications to `this` by each constructor worked:
    console.log(f.one, f.two, f.three); // logs "1 2 3"
    console.log(' ---- call methods:');
    // all methods work:
    f.foo();
    f.bar();
    f.baz();
    f.yeah();
    f.setVar();
    f.readVar();
    console.log(' --------------------------- ');
    class Lorem {
        constructor() {
            this.lo = 'rem';
        }
    }
    // class Ipsum extends multiple(Lorem, FooBar) {
    class Ipsum extends multiple(FooBar, Lorem) {
        constructor() {
            super(...arguments);
            // THIS LINE TRIGGER PROXY INFINITE RECURSION
            this.ip = 'sum';
        }
        test() {
            console.log(' -- Ipsum: call super.bar()');
            console.log(super.foo);
            console.log(super.bar);
            console.log(super.baz);
            console.log(super.yeah);
            console.log(super.setVar);
            console.log(super.readVar);
            super.bar();
            console.log(this.lo, this.ip);
        }
    }
    const i = new Ipsum();
    i.foo();
    i.bar();
    i.baz();
    i.yeah();
    i.setVar();
    i.readVar();
    i.test();
    function createOnChangeProxy(onChange, target) {
        return new Proxy(target, {
            get(target, property) {
                const item = target[property];
                if (isMutableObject(item))
                    return createOnChangeProxy(onChange, item);
                return item;
            },
            set(target, property, newValue) {
                ;
                target[property] = newValue;
                onChange();
                return true;
            },
        });
    }
    function isMutableObject(maybe) {
        if (maybe === null)
            return false;
        if (maybe instanceof Date)
            return false;
        // treat Uint8Arrays as immutable, even though they
technically aren't, because we use them a lot and we treat them as
immutable
        if (maybe instanceof Uint8Array)
            return false;
        // TODO: filter out any other special cases we can find, where
something identifies as an `object` but is effectively immutable
        return typeof maybe === 'object';
    }
    let changeCount = 0;
    const o = createOnChangeProxy(() => changeCount++, {});
    o.foo = 1;
    o.bar = 2;
    o.baz = {};
    o.baz.lorem = true;
    o.baz.yeee = {};
    o.baz.yeee.wooo = 12;
    console.log(changeCount === 6);
}
test();
# #!/JoePea (4 years ago)

Sorry you all, I realized I should've simplified it. Here's a simpler fiddle.