import { hasDomain } from "./domain.js"; import { noSuggest } from "./errors.js"; import { ancestorsOf } from "./objectKinds.js"; import { NoopBase } from "./records.js"; // even though the value we attach will be identical, we use this so classes // won't be treated as instanceof a Trait const implementedTraits = noSuggest("implementedTraits"); export const hasTrait = (traitClass) => (o) => { if (!hasDomain(o, "object")) return false; if (implementedTraits in o.constructor && o.constructor[implementedTraits].includes(traitClass)) return true; // emulate standard instanceof behavior return ancestorsOf(o).includes(traitClass); }; /** @ts-ignore required to extend NoopBase */ export class Trait extends NoopBase { static get [Symbol.hasInstance]() { return hasTrait(this); } traitsOf() { return implementedTraits in this.constructor ? this.constructor[implementedTraits] : []; } } const collectPrototypeDescriptors = (trait) => { let proto = trait.prototype; let result = {}; do { // ensure prototypes are sorted from lowest to highest precedence result = Object.assign(Object.getOwnPropertyDescriptors(proto), result); proto = Object.getPrototypeOf(proto); } while (proto !== Object.prototype && proto !== null); return result; }; export const compose = ((...traits) => { const base = function (...args) { for (const trait of traits) { const instance = Reflect.construct(trait, args, this.constructor); Object.assign(this, instance); } }; const flatImplementedTraits = []; for (const trait of traits) { // copy static properties Object.assign(base, trait); // flatten and copy prototype Object.defineProperties(base.prototype, collectPrototypeDescriptors(trait)); if (implementedTraits in trait) { // add any ancestor traits from which the current trait was composed for (const innerTrait of trait[implementedTraits]) { if (!flatImplementedTraits.includes(innerTrait)) flatImplementedTraits.push(innerTrait); } } if (!flatImplementedTraits.includes(trait)) flatImplementedTraits.push(trait); } Object.defineProperty(base, implementedTraits, { value: flatImplementedTraits, enumerable: false }); return base; }); export const implement = (...args) => { if (args[args.length - 1] instanceof Trait) return compose(...args); const implementation = args[args.length - 1]; const base = compose(...args.slice(0, -1)); // copy implementation last since it overrides traits Object.defineProperties(base.prototype, Object.getOwnPropertyDescriptors(implementation)); return base; };