From 2ac41b20e1cd49b57fb1e1ed945fa1a33097e4d6 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Mon, 18 May 2026 14:04:22 -0700 Subject: [PATCH] perf: add ES5-compatible cache for deep hierarchy method resolution When the instance shortcut optimization cannot be applied (frozen/readonly targets, Proxy objects, or when the instance already owns the property), every dynamic method call previously re-entered _getInstFunc with repeated property lookups on every invocation. This change adds a per-instance resolved-function cache stored directly in the existing instFuncTable object (keyed by className + funcName). Since instFuncTable is created with objCreate(null) during construction and Object.freeze is shallow, it remains writable even when the instance itself is frozen. Subsequent calls to the same proxy for the same instance return the cached function directly, eliminating the _getInstFunc overhead entirely. Additionally, the prototype chain walk inside _getInstFunc is now guarded behind the canAddInst check, avoiding unnecessary visited-array allocation when the instance already owns the property. Fully ES5 compatible - no WeakMap or other ES6+ APIs required. Impact: Reduces per-call overhead from O(property lookups) to O(1) cache hit for all cases where the shortcut optimization cannot be applied, particularly benefiting deep inheritance hierarchies (10+ levels). --- lib/src/DynamicProto.ts | 63 ++- lib/test/DynamicProtoDeepHierarchy.Tests.ts | 572 ++++++++++++++++++++ lib/test/Selenium/DynamicProtoTests.ts | 2 + 3 files changed, 620 insertions(+), 17 deletions(-) create mode 100644 lib/test/DynamicProtoDeepHierarchy.Tests.ts diff --git a/lib/src/DynamicProto.ts b/lib/src/DynamicProto.ts index c08b587..5120231 100644 --- a/lib/src/DynamicProto.ts +++ b/lib/src/DynamicProto.ts @@ -71,6 +71,12 @@ const DynInstChkTag = '_dynInstChk'; * tag name as the function level but a different const name for readability only. */ const DynAllowInstChkTag = DynInstChkTag; + +/** + * Prefix used for per-instance resolved function cache keys stored in the instFuncTable + * @ignore + */ +const DynResolvedCachePrefix = '_dyn_r$'; /** * The global (imported) instances where the global performance options are stored @@ -346,22 +352,25 @@ function _getInstFunc(target: any, funcName: string, proto: any, currentDynProto // If the instance already has an instance function we can't replace it let canAddInst = !objHasOwnProperty(target, funcName); - // Get current prototype - let objProto = _getObjProto(target); - let visited:any[] = []; - - // Lookup the function starting at the top (instance level prototype) and traverse down, if the first matching function - // if nothing is found or if the first hit is a dynamic proto instance then we can safely add an instance shortcut - while (canAddInst && objProto && !_isObjectArrayOrFunctionPrototype(objProto) && !_hasVisited(visited, objProto)) { - let protoFunc = objProto[funcName]; - if (protoFunc) { - canAddInst = (protoFunc === currentDynProtoProxy); - break; - } + // Only perform the prototype chain walk if canAddInst is still viable + if (canAddInst) { + // Get current prototype + let objProto = _getObjProto(target); + let visited:any[] = []; + + // Lookup the function starting at the top (instance level prototype) and traverse down, if the first matching function + // if nothing is found or if the first hit is a dynamic proto instance then we can safely add an instance shortcut + while (objProto && !_isObjectArrayOrFunctionPrototype(objProto) && !_hasVisited(visited, objProto)) { + let protoFunc = objProto[funcName]; + if (protoFunc) { + canAddInst = (protoFunc === currentDynProtoProxy); + break; + } - // We need to find all possible initial functions to ensure that we don't bypass a valid override function - visited.push(objProto); - objProto = _getObjProto(objProto); + // We need to find all possible initial functions to ensure that we don't bypass a valid override function + visited.push(objProto); + objProto = _getObjProto(objProto); + } } try { @@ -411,9 +420,25 @@ function _getProtoFunc(funcName: string, proto: any, currentDynProtoProxy: any) */ function _populatePrototype(proto:any, className:string, target:any, baseInstFuncs:any, setInstanceFunc:boolean) { function _createDynamicPrototype(proto:any, funcName:string) { + // Pre-compute a cache key for storing the resolved instance function directly in the + // instFuncTable (which is per-instance and NOT frozen even when the instance is). + // This avoids repeated _getInstFunc lookups when the shortcut optimization cannot be applied. + let cacheKey = DynResolvedCachePrefix + className + "." + funcName; + let dynProtoProxy = function() { - // Use the instance or prototype function - let instFunc = _getInstFunc(this, funcName, proto, dynProtoProxy) || _getProtoFunc(funcName, proto, dynProtoProxy); + // Fast path: check the per-instance resolved cache to avoid _getInstFunc overhead on repeated calls + let instFuncTable = this[DynInstFuncTable]; + let instFunc = instFuncTable ? instFuncTable[cacheKey] : undefined; + if (!instFunc) { + // Use the instance or prototype function + instFunc = _getInstFunc(this, funcName, proto, dynProtoProxy) || _getProtoFunc(funcName, proto, dynProtoProxy); + // Cache the resolved function for subsequent calls from this instance. + // instFuncTable is created with objCreate(null) during construction and is not frozen + // even when the instance itself is frozen (Object.freeze is shallow). + if (instFuncTable) { + instFuncTable[cacheKey] = instFunc; + } + } // eslint-disable-next-line prefer-rest-params return instFunc.apply(this, arguments); }; @@ -441,6 +466,10 @@ function _populatePrototype(proto:any, className:string, target:any, baseInstFun // Save the instance Function to the lookup table and remove it from the instance as it's not a dynamic proto function instFuncs[name] = target[name]; delete target[name]; + + // Invalidate any cached resolved function for this entry so stale + // values are not returned after re-registration via dynamicProto + delete instFuncTable[DynResolvedCachePrefix + className + "." + name]; // Add a dynamic proto if one doesn't exist or if a prototype function exists and it's not a dynamic one if (!objHasOwnProperty(proto, name) || (proto[name] && !proto[name][DynProxyTag])) { diff --git a/lib/test/DynamicProtoDeepHierarchy.Tests.ts b/lib/test/DynamicProtoDeepHierarchy.Tests.ts new file mode 100644 index 0000000..a7b8543 --- /dev/null +++ b/lib/test/DynamicProtoDeepHierarchy.Tests.ts @@ -0,0 +1,572 @@ +/// + +import dynamicProto from "../src/DynamicProto"; + +// Deep hierarchy: 10+ levels of dynamic proto inheritance to test caching performance +class DeepBase { + public callOrder: string[] = []; + public getValue?(): string; + + constructor() { + dynamicProto(DeepBase, this, (_self, base) => { + _self.getValue = () => { + return "DeepBase"; + }; + }); + } +} + +class DeepLevel1 extends DeepBase { + constructor() { + super(); + dynamicProto(DeepLevel1, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L1"; + }; + }); + } +} + +class DeepLevel2 extends DeepLevel1 { + constructor() { + super(); + dynamicProto(DeepLevel2, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L2"; + }; + }); + } +} + +class DeepLevel3 extends DeepLevel2 { + constructor() { + super(); + dynamicProto(DeepLevel3, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L3"; + }; + }); + } +} + +class DeepLevel4 extends DeepLevel3 { + constructor() { + super(); + dynamicProto(DeepLevel4, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L4"; + }; + }); + } +} + +class DeepLevel5 extends DeepLevel4 { + constructor() { + super(); + dynamicProto(DeepLevel5, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L5"; + }; + }); + } +} + +class DeepLevel6 extends DeepLevel5 { + constructor() { + super(); + dynamicProto(DeepLevel6, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L6"; + }; + }); + } +} + +class DeepLevel7 extends DeepLevel6 { + constructor() { + super(); + dynamicProto(DeepLevel7, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L7"; + }; + }); + } +} + +class DeepLevel8 extends DeepLevel7 { + constructor() { + super(); + dynamicProto(DeepLevel8, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L8"; + }; + }); + } +} + +class DeepLevel9 extends DeepLevel8 { + constructor() { + super(); + dynamicProto(DeepLevel9, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L9"; + }; + }); + } +} + +class DeepLevel10 extends DeepLevel9 { + constructor() { + super(); + dynamicProto(DeepLevel10, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L10"; + }; + }); + } +} + +// Deep hierarchy WITHOUT instance function shortcut (simulates frozen/readonly target scenario) +class DeepNoInstBase { + public callOrder: string[] = []; + public getValue?(): string; + + constructor() { + dynamicProto(DeepNoInstBase, this, (_self, base) => { + _self.getValue = () => { + return "DeepNoInstBase"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel1 extends DeepNoInstBase { + constructor() { + super(); + dynamicProto(DeepNoInstLevel1, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L1"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel2 extends DeepNoInstLevel1 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel2, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L2"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel3 extends DeepNoInstLevel2 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel3, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L3"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel4 extends DeepNoInstLevel3 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel4, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L4"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel5 extends DeepNoInstLevel4 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel5, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L5"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel6 extends DeepNoInstLevel5 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel6, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L6"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel7 extends DeepNoInstLevel6 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel7, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L7"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel8 extends DeepNoInstLevel7 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel8, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L8"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel9 extends DeepNoInstLevel8 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel9, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L9"; + }; + }, { setInstFuncs: false }); + } +} + +class DeepNoInstLevel10 extends DeepNoInstLevel9 { + constructor() { + super(); + dynamicProto(DeepNoInstLevel10, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">L10"; + }; + }, { setInstFuncs: false }); + } +} + +// Frozen target class - tests that caching works when Object.freeze prevents the shortcut +class FrozenTargetBase { + public getValue?(): string; + + constructor() { + dynamicProto(FrozenTargetBase, this, (_self, base) => { + _self.getValue = () => { + return "FrozenBase"; + }; + }); + } +} + +class FrozenTargetChild extends FrozenTargetBase { + constructor() { + super(); + dynamicProto(FrozenTargetChild, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">FrozenChild"; + }; + }); + // Freeze after dynamicProto setup - the shortcut install will fail on subsequent calls + // but the cache should still resolve the function correctly + Object.freeze(this); + } +} + +// Class that supports re-registration of dynamic functions via a public method. +// Each dynamicProto call creates a genuinely new function (not sharing a closure variable) +// so stale cache bugs are caught. +class ReRegistrableBase { + public getValue?(): string; + public reRegister: () => void; + + constructor() { + dynamicProto(ReRegistrableBase, this, (_self, base) => { + _self.getValue = () => { + return "Base:v1"; + }; + }); + + this.reRegister = () => { + dynamicProto(ReRegistrableBase, this, (_self, base) => { + _self.getValue = () => { + return "Base:v2"; + }; + }); + }; + } +} + +class ReRegistrableChild extends ReRegistrableBase { + constructor() { + super(); + dynamicProto(ReRegistrableChild, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">Child"; + }; + }); + } +} + +// Class where dynamicProto is called multiple times in the constructor (e.g. mixins / plugins pattern) +class MultiDynProtoBase { + public getA?(): string; + public getB?(): string; + + constructor() { + // First dynamicProto call sets getA + dynamicProto(MultiDynProtoBase, this, (_self, base) => { + _self.getA = () => { + return "A:v1"; + }; + }); + + // Second dynamicProto call overrides getA and sets getB + dynamicProto(MultiDynProtoBase, this, (_self, base) => { + _self.getA = () => { + return "A:v2"; + }; + _self.getB = () => { + return "B:v1"; + }; + }); + } +} + +// setInstFuncs:false variant of re-registration to test the non-shortcut cache path +class ReRegistrableNoInstBase { + public getValue?(): string; + public reRegister: () => void; + + constructor() { + dynamicProto(ReRegistrableNoInstBase, this, (_self, base) => { + _self.getValue = () => { + return "Base:v1"; + }; + }, { setInstFuncs: false }); + + this.reRegister = () => { + dynamicProto(ReRegistrableNoInstBase, this, (_self, base) => { + _self.getValue = () => { + return "Base:v2"; + }; + }, { setInstFuncs: false }); + }; + } +} + +class ReRegistrableNoInstChild extends ReRegistrableNoInstBase { + constructor() { + super(); + dynamicProto(ReRegistrableNoInstChild, this, (_self, base) => { + _self.getValue = () => { + return base.getValue() + ">Child"; + }; + }, { setInstFuncs: false }); + } +} + +export class DynamicProtoDeepHierarchyTests extends TestClass { + + public testInitialize() { + } + + public registerTests() { + this.testCase({ + name: "Deep Hierarchy: 10 levels of dynamic proto inheritance produces correct call chain", + test: () => { + let inst = new DeepLevel10(); + let result = inst.getValue(); + QUnit.assert.equal(result, "DeepBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Full 10-level call chain should resolve correctly"); + } + }); + + this.testCase({ + name: "Deep Hierarchy: Multiple instances share correct resolution", + test: () => { + let inst1 = new DeepLevel10(); + let inst2 = new DeepLevel10(); + QUnit.assert.equal(inst1.getValue(), "DeepBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "First instance resolves correctly"); + QUnit.assert.equal(inst2.getValue(), "DeepBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Second instance resolves correctly"); + // Call again to exercise the cache path + QUnit.assert.equal(inst1.getValue(), "DeepBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "First instance repeated call resolves correctly"); + QUnit.assert.equal(inst2.getValue(), "DeepBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Second instance repeated call resolves correctly"); + } + }); + + this.testCase({ + name: "Deep Hierarchy (no setInstFuncs): 10 levels without instance shortcut resolves correctly", + test: () => { + let inst = new DeepNoInstLevel10(); + let result = inst.getValue(); + QUnit.assert.equal(result, "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Full 10-level no-inst call chain should resolve correctly"); + } + }); + + this.testCase({ + name: "Deep Hierarchy (no setInstFuncs): Repeated calls use cached path", + test: () => { + let inst = new DeepNoInstLevel10(); + // First call resolves and caches + let result1 = inst.getValue(); + // Subsequent calls should use cache + let result2 = inst.getValue(); + let result3 = inst.getValue(); + QUnit.assert.equal(result1, "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "First call resolves correctly"); + QUnit.assert.equal(result2, "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Second call (cached) resolves correctly"); + QUnit.assert.equal(result3, "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Third call (cached) resolves correctly"); + } + }); + + this.testCase({ + name: "Deep Hierarchy (no setInstFuncs): Multiple instances with repeated calls", + test: () => { + let inst1 = new DeepNoInstLevel10(); + let inst2 = new DeepNoInstLevel10(); + let inst3 = new DeepNoInstLevel5(); + + // Each instance should independently cache and resolve correctly + QUnit.assert.equal(inst1.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Instance 1 first call"); + QUnit.assert.equal(inst2.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Instance 2 first call"); + QUnit.assert.equal(inst3.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5", "Instance 3 (Level5) first call"); + + // Repeated calls from cache + QUnit.assert.equal(inst1.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Instance 1 cached call"); + QUnit.assert.equal(inst2.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7>L8>L9>L10", "Instance 2 cached call"); + QUnit.assert.equal(inst3.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5", "Instance 3 (Level5) cached call"); + } + }); + + this.testCase({ + name: "Deep Hierarchy (no setInstFuncs): Mid-hierarchy instantiation works correctly", + test: () => { + let inst5 = new DeepNoInstLevel5(); + let inst7 = new DeepNoInstLevel7(); + + QUnit.assert.equal(inst5.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5", "Level5 instance resolves correctly"); + QUnit.assert.equal(inst7.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7", "Level7 instance resolves correctly"); + + // Repeated + QUnit.assert.equal(inst5.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5", "Level5 repeated call"); + QUnit.assert.equal(inst7.getValue(), "DeepNoInstBase>L1>L2>L3>L4>L5>L6>L7", "Level7 repeated call"); + } + }); + + this.testCase({ + name: "Frozen target: Dynamic proto works correctly after Object.freeze", + test: () => { + let inst = new FrozenTargetChild(); + QUnit.assert.ok(Object.isFrozen(inst), "Instance should be frozen"); + let result = inst.getValue(); + QUnit.assert.equal(result, "FrozenBase>FrozenChild", "Frozen instance should still resolve dynamic functions correctly"); + // Repeated call should work via cache + let result2 = inst.getValue(); + QUnit.assert.equal(result2, "FrozenBase>FrozenChild", "Frozen instance repeated call should resolve correctly"); + } + }); + + this.testCase({ + name: "Frozen target: Multiple frozen instances resolve independently", + test: () => { + let inst1 = new FrozenTargetChild(); + let inst2 = new FrozenTargetChild(); + QUnit.assert.ok(Object.isFrozen(inst1), "Instance 1 should be frozen"); + QUnit.assert.ok(Object.isFrozen(inst2), "Instance 2 should be frozen"); + QUnit.assert.equal(inst1.getValue(), "FrozenBase>FrozenChild", "Frozen instance 1 resolves correctly"); + QUnit.assert.equal(inst2.getValue(), "FrozenBase>FrozenChild", "Frozen instance 2 resolves correctly"); + // Cached + QUnit.assert.equal(inst1.getValue(), "FrozenBase>FrozenChild", "Frozen instance 1 cached resolves correctly"); + QUnit.assert.equal(inst2.getValue(), "FrozenBase>FrozenChild", "Frozen instance 2 cached resolves correctly"); + } + }); + + this.testCase({ + name: "Dynamic re-registration: calling dynamicProto again updates resolved function", + test: () => { + let inst = new ReRegistrableBase(); + QUnit.assert.equal(inst.getValue(), "Base:v1", "Initial call resolves to v1"); + // Call again to populate cache + QUnit.assert.equal(inst.getValue(), "Base:v1", "Second call resolves to v1 (cached)"); + + // Re-register with a new implementation + inst.reRegister(); + QUnit.assert.equal(inst.getValue(), "Base:v2", "After re-registration should resolve to v2, not stale cached v1"); + // Verify cache is updated + QUnit.assert.equal(inst.getValue(), "Base:v2", "Repeated call after re-registration should still resolve to v2"); + } + }); + + this.testCase({ + name: "Dynamic re-registration: re-registration on base does not affect other instances", + test: () => { + let inst1 = new ReRegistrableBase(); + let inst2 = new ReRegistrableBase(); + QUnit.assert.equal(inst1.getValue(), "Base:v1", "Instance 1 initial"); + QUnit.assert.equal(inst2.getValue(), "Base:v1", "Instance 2 initial"); + + inst1.reRegister(); + QUnit.assert.equal(inst1.getValue(), "Base:v2", "Instance 1 updated to v2"); + QUnit.assert.equal(inst2.getValue(), "Base:v1", "Instance 2 still v1"); + } + }); + + this.testCase({ + name: "Dynamic re-registration: child class base proxy is early-bound at construction", + test: () => { + let inst = new ReRegistrableChild(); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Initial child call chains correctly"); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Cached child call chains correctly"); + + // Re-register the base - child's base.getValue() was captured at construction time + // via _instFuncProxy, so it still references the original base function + inst.reRegister(); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Child base proxy is early-bound, still chains with original base"); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Repeated call confirms early-bound base"); + } + }); + + this.testCase({ + name: "Dynamic re-registration (no setInstFuncs): re-registration updates without instance shortcut", + test: () => { + let inst = new ReRegistrableNoInstBase(); + QUnit.assert.equal(inst.getValue(), "Base:v1", "Initial call resolves to v1"); + QUnit.assert.equal(inst.getValue(), "Base:v1", "Cached call resolves to v1"); + + inst.reRegister(); + QUnit.assert.equal(inst.getValue(), "Base:v2", "After re-registration should resolve to v2"); + QUnit.assert.equal(inst.getValue(), "Base:v2", "Cached call after re-registration resolves to v2"); + } + }); + + this.testCase({ + name: "Dynamic re-registration (no setInstFuncs): child sees updated base via late-bound proxy", + test: () => { + let inst = new ReRegistrableNoInstChild(); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Initial child call"); + QUnit.assert.equal(inst.getValue(), "Base:v1>Child", "Cached child call"); + + // With setInstFuncs:false, the base proxy captures the dynamic proxy (late-bound), + // so re-registration of the base IS visible through the child's base.getValue() + inst.reRegister(); + QUnit.assert.equal(inst.getValue(), "Base:v2>Child", "Child sees updated base via late-bound proxy"); + QUnit.assert.equal(inst.getValue(), "Base:v2>Child", "Repeated call confirms updated base"); + } + }); + + this.testCase({ + name: "Multiple dynamicProto calls in constructor: last registration wins", + test: () => { + let inst = new MultiDynProtoBase(); + QUnit.assert.equal(inst.getA(), "A:v2", "getA should be from second dynamicProto call"); + QUnit.assert.equal(inst.getB(), "B:v1", "getB should be from second dynamicProto call"); + // Cached calls + QUnit.assert.equal(inst.getA(), "A:v2", "Cached getA should still be v2"); + QUnit.assert.equal(inst.getB(), "B:v1", "Cached getB should still be v1"); + } + }); + } +} diff --git a/lib/test/Selenium/DynamicProtoTests.ts b/lib/test/Selenium/DynamicProtoTests.ts index 763c2d8..8771f65 100644 --- a/lib/test/Selenium/DynamicProtoTests.ts +++ b/lib/test/Selenium/DynamicProtoTests.ts @@ -3,6 +3,7 @@ import { DynamicProtoMultipleCallTests } from '../DynamicProtoMultipleCall.Tests import { DynamicProtoNoInstTests } from '../DynamicProtoNoInst.Tests'; import { DynamicProtoMultipleNoInstTests } from '../DynamicProtoMultipleNoInst.Tests'; import { SecurityCheckTests } from '../SecurityCheck.Tests'; +import { DynamicProtoDeepHierarchyTests } from '../DynamicProtoDeepHierarchy.Tests'; export function runTests() { new DynamicProtoDefaultTests("Default").registerTests(); @@ -10,4 +11,5 @@ export function runTests() { new DynamicProtoNoInstTests("SetInst").registerTests(); new DynamicProtoMultipleNoInstTests("Multiple SetInst").registerTests(); new SecurityCheckTests("Security Checks").registerTests(); + new DynamicProtoDeepHierarchyTests("Deep Hierarchy & Caching").registerTests(); }