diff --git a/README.md b/README.md index 224165b..e05a8c5 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ D2(0) X2(2) ``` -#### Substitute for `this` stability +## 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. @@ -199,7 +199,9 @@ For instance, one cannot use : } ``` -When constructing, the function `constructedObject` helps you retrieve the actual instance being constructed - this is the one you want to refer to. +### 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' @@ -213,6 +215,10 @@ import D, { constructedObject } from 'flat-diamond' > 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 + +Seclusion is your friend + # Seclusion Another big deal of diamond inheritance is field conflicts. @@ -241,11 +247,11 @@ As simple as that, methods (as well as accessors) of `Plane` and `DuckCourier` w ## But ... How ? And, how can I ... -When a secluded class is implemented, `this` (so, here, a `DuckCourier`) will be used as the prototype for a `Private`. A Proxy is added between `Secluded` and `Plane` to manage who is `this` in method calls (either `DuckCourier` or `Private`) - et voilĂ ! +When a secluded class is implemented, `this` (so, here, a `DuckCourier`) will be used as the prototype for a `Secluded`. A Proxy is added between `Secluded` and `Plane` to manage who is `this` in method calls (either `DuckCourier` or `Secluded`) - et voilĂ ! -Because of prototyping, `Private` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several secluded class in the legacy list will only create several "heads" who will share a prototype. +Because of prototyping, `Secluded` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several secluded class in the legacy list will only create several "heads" who will share a prototype. -`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the `privatePart` exposed by the `Secluded` class. +`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the `secluded` exposed by the `Secluded` class. ```ts import { Seclude } from 'flat-diamond' @@ -259,14 +265,16 @@ const SecludedPlane = Seclude(Plane, ['wingSpan']) class DuckCourier extends SecludedPlane { wingSpan: number = 80 get isDeviceSafe(): boolean { - return SecludedPlane.privatePart(this).wingSpan > 2 * this.wingSpan + return SecludedPlane.secluded(this).wingSpan > 2 * this.wingSpan } } ``` -## Limitations +## Seclusion and construction + +The `Secluded` will indeed be the object the `Plane` constructor built! If it was used in the references, it's perfect! -Again, the object exposed in the constructor won't be the same as the one faces from inside the secluded object' methods/accessors +## Limitations For now, only the fields can be secluded, not the methods diff --git a/src/seclude.ts b/src/seclude.ts index 563c1c0..38f06a9 100644 --- a/src/seclude.ts +++ b/src/seclude.ts @@ -4,11 +4,19 @@ import { Ctor, KeySet, Newable } from './types' import { allFLegs, bottomLeg, fLegs, nextInLine } from './utils' const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x)) - +/** + * Internally used for communication between `PropertyCollector` and `Secluded` + * This is the "baal" `Secluded` send into the "basket" (stack in case a secluded class inherits another secluded class) + * and retrieve with its data fulfilled by `PropertyCollector` + */ +interface BasketBall { + privateProperties: PropertyDescriptorMap + initialObject?: any +} export type Secluded)[]> = Newable< Omit, Keys[number]> > & { - privatePart(obj: InstanceType): InstanceType | undefined + secluded(obj: InstanceType): InstanceType | undefined } export function Seclude)[]>( base: TBase, @@ -18,7 +26,7 @@ export function Seclude ({ ...acc, [p]: true }) as KeySet, {} ), - initPropertiesBasket: PropertyDescriptorMap[] = [] + initPropertiesBasket: BasketBall[] = [] /** * In order to integrate well in diamonds, we need to be a diamond * When we create a diamond between the Secludeded and the base, the private properties of the base *have to* @@ -27,49 +35,66 @@ export function Seclude() - const diamond = fLegs(base) ? PropertyCollector : (Diamond(PropertyCollector) as TBase) + const privates = new WeakMap(), + diamondSecluded = !fLegs(base), + diamond = diamondSecluded ? (Diamond(PropertyCollector) as TBase) : PropertyCollector class Secluded extends (diamond as any) { - static privatePart(obj: TBase): TBase | undefined { + static secluded(obj: TBase): TBase | undefined { return privates.get(obj) } constructor(...args: any[]) { - const init: PropertyDescriptorMap = {} + const init: BasketBall = { privateProperties: {} } initPropertiesBasket.unshift(init) try { super(...args) } finally { initPropertiesBasket.shift() } - const actThis = constructedObject(this) - privates.set( - actThis, - Object.create( - // This proxy is used to write public properties in the prototype (the public object) - new Proxy(actThis, { - set(target, p, value, receiver) { - Object.defineProperty(p in secludedProperties ? receiver : target, p, { - value, - writable: true, - enumerable: true, - configurable: true - }) - return true - }, - getPrototypeOf: (target) => target - }), - init - ) - ) + const actThis = constructedObject(this), + // This proxy is used to write public properties in the prototype (the public object) + protoProxy = new Proxy(actThis, { + set(target, p, value, receiver) { + Object.defineProperty(p in secludedProperties ? receiver : target, p, { + value, + writable: true, + enumerable: true, + configurable: true + }) + return true + }, + getPrototypeOf: (target) => target + }) + let secluded: InstanceType + /* Here, what happens: + `init.initialObject` is the instance of the secluded class who contains all its public properties + `init.privateProperties` is a pure object containing all its private properties + We need the initial object but with only the private properties + 1- we remove all the public ones (they have been transferred to `constructedObject`) + 2- we restore the private ones + */ + // Except when we're the main class, or when the secluded is a diamond, then we create a new object + if (!diamondSecluded) secluded = Object.create(protoProxy, init.privateProperties) + else { + secluded = init.initialObject! + for (const p of [ + ...Object.getOwnPropertyNames(secluded), + ...Object.getOwnPropertySymbols(secluded) + ]) + delete secluded[p] + Object.defineProperties(secluded, init.privateProperties) + Object.setPrototypeOf(secluded, protoProxy) + } + privates.set(actThis, secluded) } } function whoAmI(receiver: TBase) { @@ -114,7 +139,12 @@ export function Seclude { - if (p === 'constructor') return fakeCtor + switch (p) { + case 'constructor': + return fakeCtor + case Symbol.toStringTag: + return `[Secluded<${target.name}>]` + } const actor = whoAmI(receiver) if (p in target.prototype) { const pd = nextInLine(target, p)! diff --git a/test/protect.test.ts b/test/seclude.test.ts similarity index 74% rename from test/protect.test.ts rename to test/seclude.test.ts index 844f095..7709453 100644 --- a/test/protect.test.ts +++ b/test/seclude.test.ts @@ -1,4 +1,4 @@ -import Diamond, { Seclude } from '../src' +import Diamond, { constructedObject, Seclude } from '../src' interface Scenario { pubFld: number @@ -12,7 +12,7 @@ interface Scenario { set accFld(v: number) } -function testScenario(t: Scenario, P: { privatePart(t: any): any }) { +function testScenario(t: Scenario, P: { secluded(t: any): any }) { expect(t.pubFld).toBe(0) expect(t.accFld).toBe(8) expect(t.prvFld).toBe(10) @@ -27,13 +27,18 @@ function testScenario(t: Scenario, P: { privatePart(t: any): any }) { t.setPrvFld(5) expect(t.accFld).toBe(5) expect(t.prvFld).toBe(2) - const pp = P.privatePart(t) + const pp = P.secluded(t) expect(pp?.prvFld).toBe(5) expect(pp?.pubFld).toBe(4) + return t } test('leg-less', () => { + let builtX: X | null = null class X { + constructor() { + builtX = this + } pubFld = 0 prvFld = 8 setPrvFld(v: number) { @@ -61,11 +66,16 @@ test('leg-less', () => { prvFld = 10 } - testScenario(new Y(), P) + let t = testScenario(new Y(), P) + expect(builtX).toBe(P.secluded(t)) }) test('leg-half', () => { + let builtX: X | null = null class X { + constructor() { + builtX = this + } pubFld = 0 prvFld = 8 setPrvFld(v: number) { @@ -95,11 +105,15 @@ test('leg-half', () => { class D extends Diamond(P, Y) {} class E extends Diamond(Y, P) {} - testScenario(new D(), P) - testScenario(new E(), P) + let t = testScenario(new D(), P) + expect(builtX).toBe(P.secluded(t)) + t = testScenario(new E(), P) + expect(builtX).toBe(P.secluded(t)) }) test('leg-full', () => { + // Note: the scenario is so unrealistic (if X is aware of Diamonds, it needs no seclusion) that no way to find the + // 'secluded' object is provided - hence there is no test on that part here class X extends Diamond() { pubFld = 0 prvFld = 8