trusktr-dummy-test-pkg
Version:
JavaScript/TypeScript class inheritance tools.
346 lines • 15.8 kB
JavaScript
// --- TODO handle static inheritance. Nothing has been implemented with regards to
// static inheritance yet.
// --- TODO allow the subclass (f.e. the `Foo` in `class Foo extends multiple(One,
// Two, Three) {}`) to call each super constructor (One, Two, and Three)
// individually with specific arguments.
// --- TODO Prevent duplicate classes in the "prototype tree". F.e. if someone calls
// `multiple(One, Two, Three)`, and `Three` already includes `Two`, we can
// discard the `Two` argument and perform the combination as if `multiple(One,
// Three)` had been called.
// --- TODO cache the results, so more than one call to `multiple(One, Two, Three)`
// returns the same class reference as the first call.
// --- TODO, allow the user to handle the diamond problem in some way other than
// ("property or method from the first class in the list wins"). Perhaps require
// the user to specify which method to call. For now, it simply calls the first
// method in the order in which the classes were passed into multiple(). Look
// here for ideas based on how different languages handle it:
// https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem
var ImplementationMethod;
(function (ImplementationMethod) {
ImplementationMethod["PROXIES_ON_INSTANCE_AND_PROTOTYPE"] = "PROXIES_ON_INSTANCE_AND_PROTOTYPE";
ImplementationMethod["PROXIES_ON_PROTOTYPE"] = "PROXIES_ON_PROTOTYPE";
// TODO, This will be similar to PROXIES_ON_INSTANCE_AND_PROTOTYPE, but
// instead of placing a proxy on the instance, place a Proxy as a direct
// prototype of the instance. I think this should work with Custom Elements,
// and unlike PROXIES_ON_PROTOTYPE, super calls won't access own properties
// on the instance, but actually on the prototypes (test5 super access tests
// fail with PROXIES_ON_PROTOTYPE method).
ImplementationMethod["PROXY_AFTER_INSTANCE_AND_PROTOTYPE"] = "PROXY_AFTER_INSTANCE_AND_PROTOTYPE";
})(ImplementationMethod || (ImplementationMethod = {}));
export function makeMultipleHelper(options) {
/**
* Mixes the given classes into a single class. This is useful for multiple
* inheritance.
*
* @example
* class Foo {}
* class Bar {}
* class Baz {}
* class MyClass extends multiple(Foo, Bar, Baz) {}
*/
// ------------ method 1, define the `multiple()` signature with overrides. The
// upside is it is easy to understand, but the downside is that name collisions
// in properties cause the collided property type to be `never`. This would make
// it more difficult to provide solution for the diamond problem.
// ----------------
// function multiple(): typeof Object
// function multiple<T extends Constructor>(classes: T): T
// function multiple<T extends Constructor[]>(...classes: T): Constructor<ConstructorUnionToInstanceTypeUnion<T[number]>>
// function multiple(...classes: any): any {
//
// ------------ method 2, define the signature of `multiple()` with a single
// signature. The upside is this picks the type of the first property
// encountered when property names collide amongst all the classes passed into
// `multiple()`, but the downside is the inner implementation may require
// casting, and this approach can also cause an infinite type recursion
// depending on the types used inside the implementation.
// ----------------
return function multiple(...classes) {
const mode = (options && options.method) || ImplementationMethod.PROXIES_ON_INSTANCE_AND_PROTOTYPE;
switch (mode) {
case ImplementationMethod.PROXIES_ON_INSTANCE_AND_PROTOTYPE: {
return withProxiesOnThisAndPrototype(...classes);
}
case ImplementationMethod.PROXIES_ON_PROTOTYPE: {
return withProxiesOnPrototype(...classes);
}
case ImplementationMethod.PROXY_AFTER_INSTANCE_AND_PROTOTYPE: {
throw new Error(' not implemented yet');
}
}
};
}
/**
* Mixes the given classes into a single class. This is useful for multiple
* inheritance.
*
* @example
* class Foo {}
* class Bar {}
* class Baz {}
* class MyClass extends multiple(Foo, Bar, Baz) {}
*/
export const multiple = makeMultipleHelper({ method: ImplementationMethod.PROXIES_ON_INSTANCE_AND_PROTOTYPE });
// export const multiple = makeMultipleHelper({method: ImplementationMethod.PROXIES_ON_PROTOTYPE})
function withProxiesOnThisAndPrototype(...classes) {
// avoid performance costs in special cases
if (classes.length === 0)
return Object;
if (classes.length === 1)
return classes[0];
const FirstClass = classes.shift();
// inherit the first class normally. This allows for required native
// inheritance in certain special cases (like inheriting from HTMLElement
// when making Custom Elements).
class MultiClass extends FirstClass {
constructor(...args) {
super(...args);
const instances = [];
// make instances of the other classes to get/set properties on.
let Ctor;
for (let i = 0, l = classes.length; i < l; i += 1) {
Ctor = classes[i];
const instance = Reflect.construct(Ctor, args);
instances.push(instance);
}
return new Proxy(this, {
// No `set()` trap is needed in this Proxy handler, at least for
// the tests so far. Methods automatically have the correct
// receiver when the are gotten with the `get()` trap, so if any
// methods set a property, the set happens on the expected
// instance, just like regular [[Set]].
get(target, key, self) {
if (Reflect.ownKeys(target).includes(key))
return Reflect.get(target, key, self);
let instance;
for (let i = 0, l = instances.length; i < l; i += 1) {
instance = instances[i];
if (Reflect.ownKeys(instance).includes(key))
return Reflect.get(instance, key, self);
}
const proto = Object.getPrototypeOf(self);
if (Reflect.has(proto, key))
return Reflect.get(proto, key, self);
return undefined;
},
ownKeys(target) {
let keys = Reflect.ownKeys(target);
let instance;
let instanceKeys;
for (let i = 0, l = instances.length; i < l; i += 1) {
instance = instances[i];
instanceKeys = Reflect.ownKeys(instance);
for (let j = 0, l = instanceKeys.length; j < l; j += 1)
keys.push(instanceKeys[j]);
}
return keys;
},
// This makes the `in` operator work, for example.
has(target, key) {
if (Reflect.ownKeys(target).includes(key))
return true;
let instance;
for (let i = 0, l = instances.length; i < l; i += 1) {
instance = instances[i];
if (Reflect.ownKeys(instance).includes(key))
return true;
}
// all instances share the same prototype, so just check it once
const proto = Object.getPrototypeOf(self);
if (Reflect.has(proto, key))
return true;
return false;
},
});
}
}
const newMultiClassPrototype = new Proxy(Object.create(FirstClass.prototype), {
get(target, key, self) {
if (Reflect.has(target, key))
return Reflect.get(target, key, self);
let Class;
for (let i = 0, l = classes.length; i < l; i += 1) {
Class = classes[i];
if (Reflect.has(Class.prototype, key))
return Reflect.get(Class.prototype, key, self);
}
},
has(target, key) {
if (Reflect.has(target, key))
return true;
let Class;
for (let i = 0, l = classes.length; i < l; i += 1) {
Class = classes[i];
if (Reflect.has(Class.prototype, key))
return true;
}
return false;
},
});
// This is so that `super` calls will work. We can't replace
// MultiClass.prototype with a Proxy because MultiClass.prototype is
// non-configurable, so it is impossible to wrap it with a Proxy. Instead,
// we stick our own custom Proxy-wrapped prototype object between
// MultiClass.prototype and FirstClass.prototype.
Object.setPrototypeOf(MultiClass.prototype, newMultiClassPrototype);
return MultiClass;
}
let currentSelf = [];
const __instances__ = new WeakMap();
const getInstances = (inst) => {
let result = __instances__.get(inst);
if (!result)
__instances__.set(inst, (result = []));
return result;
};
const getResult = { has: false, value: undefined };
function getFromInstance(instance, key, result) {
result.has = false;
result.value = undefined;
if (Reflect.ownKeys(instance).includes(key)) {
result.has = true;
result.value = Reflect.get(instance, key);
return;
}
const instances = __instances__.get(instance);
if (!instances)
return;
for (const instance of instances) {
// if (hasKey(instance, key, true)) {
// getFromInstance(instance, key, result)
// return
// }
getFromInstance(instance, key, result);
if (result.has)
return;
}
}
let shouldGetFromPrototype = false;
let topLevelMultiClassPrototype = null;
function withProxiesOnPrototype(...classes) {
// avoid performance costs in special cases
if (classes.length === 0)
return Object;
if (classes.length === 1)
return classes[0];
const FirstClass = classes.shift();
// inherit the first class normally. This allows for required native
// inheritance in certain special cases (like inheriting from HTMLElement
// when making Custom Elements).
class MultiClass extends FirstClass {
constructor(...args) {
super(...args);
// This assumes no super constructor returns a different this from
// their constructor. Otherwise the getInstances call won't work as
// expected.
const instances = getInstances(this);
// make instances of the other classes to get/set properties on.
for (const Ctor of classes) {
const instance = Reflect.construct(Ctor, args);
instances.push(instance);
}
}
}
const newMultiClassPrototype = new Proxy(Object.create(FirstClass.prototype), {
get(target, key, self) {
if (!topLevelMultiClassPrototype)
topLevelMultiClassPrototype = target;
if (!shouldGetFromPrototype) {
getFromInstance(self, key, getResult);
if (getResult.has) {
topLevelMultiClassPrototype = null;
return getResult.value;
}
// only the top level MultiClass subclass prototype will check
// instances for a property. The superclass MultiClass
// prototypes will do a regular prototype get.
shouldGetFromPrototype = true;
}
// TODO, I think instead of passing `self` we should be passing the
// instances created from the classes? We need to write more tests,
// especially ones that create new properties later and not at
// construction time.
if (shouldGetFromPrototype) {
let result = undefined;
if (Reflect.has(target, key))
result = Reflect.get(target, key, self);
let Class;
for (let i = 0, l = classes.length; i < l; i += 1) {
Class = classes[i];
if (Reflect.has(Class.prototype, key))
result = Reflect.get(Class.prototype, key, self);
}
if (topLevelMultiClassPrototype === target) {
topLevelMultiClassPrototype = null;
shouldGetFromPrototype = false;
}
return result;
}
// currentSelf.push(self)
// if (Reflect.ownKeys(self).includes(key)) {
// currentSelf.pop()
// return Reflect.get(target, key, self)
// }
// currentSelf.pop()
// for (const instance of getInstances(self)) {
// currentSelf.push(instance)
// if (Reflect.ownKeys(instance).includes(key)) {
// currentSelf.pop()
// return Reflect.get(instance, key, instance)
// }
// currentSelf.pop()
// }
// return undefined
},
set(target, key, value, self) {
currentSelf.push(self);
// If the key is in the current prototype chain, continue like normal...
if (Reflect.has(target, key)) {
currentSelf.pop();
return Reflect.set(target, key, value, self);
}
currentSelf.pop();
// ...Otherwise if the key isn't, set it on one of the instances of the classes.
for (const instance of getInstances(self)) {
currentSelf.push(instance);
if (Reflect.has(instance, key)) {
currentSelf.pop();
return Reflect.set(instance, key, value, instance);
// return Reflect.set(instance, key, value, self)
}
currentSelf.pop();
}
// If the key is not found, set it like normal.
return Reflect.set(target, key, value, self);
},
has(target, key) {
// if (currentSelf.length) {
// let current = currentSelf[currentSelf.length - 1]
// while (current) {
// if (Reflect.ownKeys(current).includes(key)) return true
// current = Reflect.getPrototypeOf(current) as MultiClass
// }
// for (const instance of getInstances(current as MultiClass))
// if (Reflect.has(instance, key)) return true
// } else {
if (Reflect.has(target, key))
return true;
let Class;
for (let i = 0, l = classes.length; i < l; i += 1) {
Class = classes[i];
if (Reflect.has(Class.prototype, key))
return true;
}
// }
return false;
},
});
// This is so that `super` calls will work. We can't replace
// MultiClass.prototype with a Proxy because MultiClass.prototype is
// non-configurable, so it is impossible to wrap it with a Proxy. Instead,
// we stick our own custom Proxy-wrapped prototype object between
// MultiClass.prototype and FirstClass.prototype.
Object.setPrototypeOf(MultiClass.prototype, newMultiClassPrototype);
return MultiClass;
}
//# sourceMappingURL=multiple.js.map