diff --git a/README.md b/README.md index af533ec..4ddfc8f 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,11 @@ testC.method() In the first case (`testA.method()`), the `super.method()` will call the one defined in `X`. In the second case (`testC.method()`), when the one from class `A` will be invoked, the `super.method()` will call the one defined in `B` who will in turn call the one defined in `X` -### A bit made up ? +Also note that `Diamond(...)` are useful even when inheriting no or one class: The `Diamond` between `A` and `X` here allows the library to reorganize the classes. Without it, the `super` of `A` would always end up in `X`. -No, and even well constructed! In the previous example (`C - A - B - X`), invoking `new C()` will invoke the constructors of `C`, `A`, `B` then `X` in sequence (roughly) +### A bit made up ? -> :warning: Not on the same object, even if it really feels the same. -> When constructing, `this` is a temporary object and it cannot be used as a reference - as discussed [below](#substitute-for-this-stability) +No, and even well constructed! In the previous example (`C - A - B - X`), invoking `new C()` will invoke the constructors of `C`, `A`, `B` then `X` in sequence. ## But ... How ? @@ -96,7 +95,7 @@ let testA = new Xa(), When constructing `Xa`, the constructor of `A` will be invoked with `this` being of class `A` -When constructing `Xb`, the constructor of `B` will be invoked with `this` being (after `super()`) of class `Xb` +When constructing `Xb`, the constructor of `B` will be invoked with `this` being (after `super()`) the constructed diamond (here, `Xb`) > Note: This is the only difference made by using `extend D()` for root classes @@ -160,7 +159,7 @@ The only problem still worked on is that if a class who has no implementation fo > Note: [It seems impossible...](https://stackoverflow.com/questions/79149281/complex-types-definition-abstract-method-filter) -### Construction concern +### Constructor parameters The main concern is about the fact that a class can think it extends directly another and another class can "come" in between in some legacy schemes. It is mainly concerning for constructors. @@ -188,40 +187,9 @@ D2(0) X2(2) ``` -## Substitute for `this` stability - -Because constructors cannot be invoked like other functions (it was simpler in older days), the objects have to be duplicated on construction. - -For instance, one cannot use : - -```ts - constructor() { - super() - instances.register(this); - } -``` - -> Note: Indeed, this should still just work fine in most case as `Diamond` modifies the temporary object for it to become a straight "forward" to the `Diamond` object (anyway doomed to be garbage collected if not used) - it wouldn't be the same reference but the same behavior with the same data. - -### When we are writing the class - -When constructing a class who inherit (directly or indirectly) a `Diamond`, the function `constructedObject` helps you retrieve the actual instance being constructed - this is the one you want to refer to. - -```ts -import D, { constructedObject } from 'flat-diamond' -... - constructor() { - super() - instances.register(constructedObject(this)) - } -``` - -> Note: There is no need to modify it directly, all the properties initialized on the temporary object are going to be transposed on it -> Note: `constructedObject(this)` will return a relevant value _only_ in classes extending `Diamond(...)` after `super(...)` - -### When the class is from a library +### Construction concern -[Seclusion](#seclusion-and-construction) is your friend +Building another diamond in the constructor before calling `super(...)` will break the game. (Fields are initialized when `super` returns - no worries there) # Seclusion diff --git a/package.json b/package.json index 96367ae..826eb88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flat-diamond", - "version": "1.0.6", + "version": "1.0.7", "types": "./lib/index.d.ts", "exports": { ".": { diff --git a/src/diamond.ts b/src/diamond.ts index 98c72a4..803d6d0 100644 --- a/src/diamond.ts +++ b/src/diamond.ts @@ -1,5 +1,5 @@ import { Ctor, HasBases, Newable } from './types' -import { allFLegs, bottomLeg, fLegs, forwardProxyHandler, nextInFLeg } from './utils' +import { allFLegs, bottomLeg, emptySecludedProxyHandler, fLegs, nextInFLeg } from './utils' type BuildingStrategy = Map let buildingDiamond: { @@ -44,13 +44,6 @@ export function hasInstanceManager(cls: Class) { } } -function forwardTempTo(target: any, temp: any) { - if (target === temp) return - Object.defineProperties(target, Object.getOwnPropertyDescriptors(temp)) - for (const p of Object.getOwnPropertyNames(temp)) delete temp[p] - Object.setPrototypeOf(temp, new Proxy(target, forwardProxyHandler)) -} - export default function Diamond( ...baseClasses: TBases ): Newable> { @@ -90,12 +83,28 @@ export default function Diamond( // `super()`: Builds the temporary objects and import all their properties for (const subs of responsibility) { buildingDiamond = fLegs(subs) ? locallyStoredDiamond : null - const temp = new (subs as any)(...args) // `any` because declared as an abstract class + //@ts-expect-error subs is declared as abstract + const temp = new subs(...args) // Even if `Diamond` managed: property initializers do not go through proxy - forwardTempTo(locallyStoredDiamond.built, temp) + if (locallyStoredDiamond.built !== temp) { + // import properties from temp object + Object.defineProperties( + locallyStoredDiamond.built, + Object.getOwnPropertyDescriptors(temp) + ) + // Empty the object and make it react as if it was the diamond. + // Useless in most cases, but if that object was given out as a reference, it can still + // be interacted with + for (const p of Object.getOwnPropertyNames(temp)) delete temp[p] + // TODO: test this fake "head" + Object.setPrototypeOf( + temp, + new Proxy(locallyStoredDiamond.built, emptySecludedProxyHandler) + ) + } } } finally { - forwardTempTo(locallyStoredDiamond.built, this) + //In the constructor method and in the field initializers, we can build diamonds, but not *this* diamond buildingDiamond = null } return locallyStoredDiamond.built diff --git a/src/seclude.ts b/src/seclude.ts index f771879..e4ab7a4 100644 --- a/src/seclude.ts +++ b/src/seclude.ts @@ -1,6 +1,13 @@ import Diamond, { diamondHandler, hasInstanceManager } from './diamond' import { Ctor, KeySet, Newable } from './types' -import { allFLegs, bottomLeg, fLegs, nextInLine, secludedPropertyDescriptor } from './utils' +import { + allFLegs, + bottomLeg, + fLegs, + nextInLine, + secludedPropertyDescriptor, + secludedProxyHandler +} from './utils' const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x)) /** @@ -23,7 +30,8 @@ export type Secluded< } export function Seclude)[]>( base: TBase, - properties: Keys = [] as any as Keys + //@ts-expect-error Cannot convert `never[]` to `Keys` + properties: Keys = [] ): Secluded { const secludedProperties: KeySet = properties.reduce( (acc, p) => ({ ...acc, [p]: true }) as KeySet, @@ -50,8 +58,7 @@ export function Seclude(), diamondSecluded = !fLegs(base), - // `any` because newable -> abstract - diamond = diamondSecluded ? (Diamond(PropertyCollector) as any) : PropertyCollector + diamond = diamondSecluded ? Diamond(PropertyCollector) : PropertyCollector // We make sure `Secluded(X).secluded(x) instanceof X` if (diamondSecluded) { Object.defineProperty(base, Symbol.hasInstance, { @@ -59,7 +66,7 @@ export function Seclude target - }) + //@ts-expect-error ProxyHandler ?? + protoProxy = new Proxy(this, secludedProxyHandler(base, secludedProperties)) let secluded: InstanceType /* Here, what happens: `init.initialObject` is the instance of the secluded class who contains all its public properties diff --git a/src/utils.ts b/src/utils.ts index 19467c3..0aecd57 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Ctor } from './types' +import { Ctor, KeySet } from './types' /** * Gives all the classes from the base up to just before Object @@ -26,6 +26,9 @@ export function bottomLeg(ctor: Ctor) { return last } +/** + * Get the next property descriptor of `name` in the lineage + */ export function nextInLine(ctor: Ctor, name: PropertyKey) { let rv: PropertyDescriptor | undefined for (const uniLeg of linearLeg(ctor)) @@ -33,6 +36,9 @@ export function nextInLine(ctor: Ctor, name: PropertyKey) { return rv === secludedPropertyDescriptor ? undefined : rv } +/** + * Get the fLegs of the bottom class of this lineage + */ export function fLegs(ctor: Ctor) { return allFLegs.get(bottomLeg(ctor)) } @@ -66,36 +72,30 @@ export function nextInFLeg(ctor: Ctor, name: PropertyKey, diamond: Ctor) { export const allFLegs = new WeakMap() -// Deflect all actions so they they apply to `target` instead of `receiver` -export const forwardProxyHandler: ProxyHandler = { - get(target, p) { - return Reflect.get(target, p) - }, - set(target, p, v) { - return Reflect.set(target, p, v) - }, - getOwnPropertyDescriptor(target, p) { - return Reflect.getOwnPropertyDescriptor(target, p) - }, - getPrototypeOf(target) { - return Reflect.getPrototypeOf(target) - }, - ownKeys(target) { - return Reflect.ownKeys(target) - }, - has(target, p) { - return Reflect.has(target, p) - }, - isExtensible(target) { - return Reflect.isExtensible(target) - }, - preventExtensions(target) { - return Reflect.preventExtensions(target) - }, - defineProperty(target, p, attributes) { - return Reflect.defineProperty(target, p, attributes) - }, - deleteProperty(target, p) { - return Reflect.deleteProperty(target, p) - } +export function secludedProxyHandler( + base: TBase | null, + secludedProperties: KeySet +) { + return { + get(target, p, receiver) { + if (base && p in base.prototype) { + const pd = nextInLine(base, p) + return pd && (pd.value || pd.get!.call(receiver)) + } + return p in secludedProperties ? undefined : Reflect.get(target, p, receiver) + }, + set(target, p, value, receiver) { + if (p in secludedProperties) + Object.defineProperty(receiver, p, { + value, + writable: true, + enumerable: true, + configurable: true + }) + else return Reflect.set(target, p, value, target) + return true + }, + getPrototypeOf: (target) => target + } as ProxyHandler } +export const emptySecludedProxyHandler = secludedProxyHandler(null, {}) diff --git a/test/dynamic.test.ts b/test/dynamic.test.ts index 196c3a0..6710476 100644 --- a/test/dynamic.test.ts +++ b/test/dynamic.test.ts @@ -36,9 +36,12 @@ test('dynamic diamond', () => { return x + 42 } expect(d.method(0)).toBe(46) - expect(() => (d as any).unexpected()).toThrow() - ;(B.prototype as any).unexpected = function (this: B) { + //@ts-ignore + expect(() => d.unexpected()).toThrow() + //@ts-ignore + B.prototype.unexpected = function (this: B) { return 202 } - expect((d as any).unexpected()).toBe(202) + //@ts-ignore + expect(d.unexpected()).toBe(202) }) diff --git a/test/seclude.test.ts b/test/seclude.test.ts index 3aa47ae..5346d70 100644 --- a/test/seclude.test.ts +++ b/test/seclude.test.ts @@ -66,7 +66,8 @@ function testScenario(t: Scenario, P: { secluded(t: any): any }) { expect(pp?.pubFld).toBe(4) //methods expect(t.methodA()).toBe('yXaXb') - expect(() => (t as any).methodB()).toThrow() + //@ts-expect-error + expect(() => t.methodB()).toThrow() return t }