From 7b403cf0acd377bd9bb676fa18f7e368fe18309a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Marie=20De=20Mey?= Date: Thu, 7 Nov 2024 08:15:02 +0200 Subject: [PATCH] instanceof secluded --- CHANGELOG.md | 13 +++++++++++++ README.md | 14 ++++++++++++-- src/diamond.ts | 4 +++- src/seclude.ts | 6 +++--- src/utils.ts | 21 ++++++++++++++------- test/abcd.test.ts | 4 ++++ 6 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9b0d787 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# 1.0.10 + +## Bug-fix + +- `KeySet` now rooted on `Object.create(null)` instead of `{}` so that `'constructor' in secludedProperties` is false +- Temporary object (modified as head with `this` diamond as proxy) now has good constructor and `instanceof` + +# 1.0.9 + +## Change + +- `.secluded(...)` is removed as `Secluded` class becomes the function +- `instanceOf` helper is removed as classes used in diamonds have their `hasInstance` modified and `instanceof` keyword is enough \ No newline at end of file diff --git a/README.md b/README.md index 147e8db..f5f73cd 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,15 @@ Also note that `Diamond(...)` are useful even when inheriting no or one class: T 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. +# Explanations and edge cases + +> :bell: The following is for details on edge cases and explanations on "how to" and how it works. The documentation needed to be able to use it is above. + ## But ... How ? The class created by the library (`Diamond(A, B)`) has a `Proxy` `prototype` (understand who can) who allow accessing the whole legacy of an object, from its constructor 'flat legacy'. -### There is no `get constructor()` +## There is no `get constructor()` The construction scheme is a bit complex as a `Diamond` class is geared to build its legacy and the legacy of others. @@ -219,7 +223,7 @@ When a secluded class is implemented (here, a `Plane`), the instance prototype w 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 fact a `Secluded` class is also a function to retrieve a private part. +`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the fact a `Secluded` class is also a function to retrieve the private part. ```ts import { Seclude } from 'flat-diamond' @@ -238,6 +242,8 @@ class DuckCourier extends MountedPlane { } ``` +> Note: `MountedPlane(this)` returns directly the instance with the good prototype for the private parts - hence, there is no need to `bind`, `.call(...)` or `.apply(...)` functions. + ## Seclusion and... ### ...construction @@ -250,6 +256,10 @@ Yes, it's okay... So, secluding can also be useful when some class specify different methods with the same name, so that each has its unique version (unaccessible from outside) +### ...diamonds + +If `A` extends `B` who extends a `Diamond(...)`, the fields and methods of `A` and `B` only will be secluded. + # Real world ## Purpose diff --git a/src/diamond.ts b/src/diamond.ts index 4755f2f..608060f 100644 --- a/src/diamond.ts +++ b/src/diamond.ts @@ -1,7 +1,6 @@ import { Ctor, HasBases, Newable } from './types' import { allFLegs, - bottomLeg, emptySecludedProxyHandler, fLegs, hasInstanceManager, @@ -14,6 +13,9 @@ import { type BuildingStrategy = Map let buildingDiamond: { built: object + // TODO: Not a map, should be a stack where each elements are popped one by one, we know the order + // It could allow 'Inconsistent diamond hierarchy' to become a warning? + // (the error would be checked when exiting the stack) strategy: BuildingStrategy } | null = null diff --git a/src/seclude.ts b/src/seclude.ts index 869694b..97d3685 100644 --- a/src/seclude.ts +++ b/src/seclude.ts @@ -9,7 +9,7 @@ import { secludedProxyHandler } from './utils' -const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x)) +const publicPart = (x: T): T => 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) @@ -35,7 +35,7 @@ export function Seclude { const secludedProperties: KeySet = properties.reduce( (acc, p) => ({ ...acc, [p]: true }) as KeySet, - {} + Object.create(null) ), initPropertiesBasket: BasketBall[] = [] const privates = new WeakMap(), @@ -117,7 +117,7 @@ export function Seclude( ) { return { get(target, p, receiver) { + // `p in base.prototype` => access secluded instance method 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) + return p in secludedProperties ? undefined : Reflect.get(target, p, target) }, set(target, p, value, receiver) { - if (p in secludedProperties) + 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 + return true + } + return Reflect.set(target, p, value, target) }, getPrototypeOf: (target) => target } as ProxyHandler } -export const emptySecludedProxyHandler = secludedProxyHandler(null, {}) +export const emptySecludedProxyHandler = secludedProxyHandler(null, Object.create(null)) export function hasInstanceManager( cls: Class, diff --git a/test/abcd.test.ts b/test/abcd.test.ts index 2ff2df5..135d9c7 100644 --- a/test/abcd.test.ts +++ b/test/abcd.test.ts @@ -1,9 +1,12 @@ import Diamond from '../src' import { log, logs } from './logger' +let builtA: A | undefined + abstract class A { constructor(public arg: string) { this.log('construct A') + builtA = this } log(...args: any[]) { log(`[class=${this.constructor.name}]`, ...args) @@ -59,6 +62,7 @@ beforeEach(() => { test('call orders', () => { const obj = new D('o') + expect(builtA instanceof A).toBe(true) expect(logs()).toEqual([ '[class=A] construct A', '[class=D] construct B',