From 581e4197d9546834d61e52f4d4c1cff7bcdcf87d Mon Sep 17 00:00:00 2001 From: nathanschmid08 Date: Wed, 29 Jul 2026 20:38:50 +0200 Subject: [PATCH 1/4] feat(reactivity): track injecting children on parent components and clean up on child unmount --- lib/core/runtime/AvenxComponent.js | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/core/runtime/AvenxComponent.js b/lib/core/runtime/AvenxComponent.js index 9939db5..9207934 100644 --- a/lib/core/runtime/AvenxComponent.js +++ b/lib/core/runtime/AvenxComponent.js @@ -892,6 +892,13 @@ export class AvenxComponent { this.renderWatcher.teardown(); } + if (this._injectedAncestors) { + for (const { ancestor, key } of this._injectedAncestors) { + ancestor.__unregisterInjectingChild(key, this); + } + this._injectedAncestors = []; + } + if (this._watchers) { for (const watcher of this._watchers) { watcher.teardown(); @@ -1032,6 +1039,18 @@ export class AvenxComponent { ); return undefined; } + + if (!this._injectedAncestors) { + this._injectedAncestors = []; + } + const alreadyRegistered = this._injectedAncestors.some( + (reg) => reg.ancestor === ancestor && reg.key === provideKey + ); + if (!alreadyRegistered) { + ancestor.__registerInjectingChild(provideKey, this); + this._injectedAncestors.push({ ancestor, key: provideKey }); + } + return this.#getAncestorProvidedValue(ancestor, provideKey); }, enumerable: true, @@ -1143,6 +1162,37 @@ export class AvenxComponent { } } + /** + * Registers a child component that is injecting a provided key. + * @param {string} key - The provided key. + * @param {AvenxComponent} child - The child component. + */ + __registerInjectingChild(key, child) { + if (!this._injectingChildren) { + this._injectingChildren = new Map(); + } + let children = this._injectingChildren.get(key); + if (!children) { + children = new Set(); + this._injectingChildren.set(key, children); + } + children.add(child); + } + + /** + * Unregisters an injecting child component. + * @param {string} key - The provided key. + * @param {AvenxComponent} child - The child component. + */ + __unregisterInjectingChild(key, child) { + if (this._injectingChildren) { + const children = this._injectingChildren.get(key); + if (children) { + children.delete(child); + } + } + } + /** * Retrieves a property or method from the component's scope. * Used for array-based provide to resolve keys dynamically. From 1d49e9abd1c7ddf8680bf6cc6ab4916a24a33faa Mon Sep 17 00:00:00 2001 From: nathanschmid08 Date: Wed, 29 Jul 2026 20:39:05 +0200 Subject: [PATCH 2/4] feat(reactivity): trigger injecting children update check when provided parent value changes --- lib/core/runtime/AvenxComponent.js | 39 +++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/core/runtime/AvenxComponent.js b/lib/core/runtime/AvenxComponent.js index 9207934..d9a262e 100644 --- a/lib/core/runtime/AvenxComponent.js +++ b/lib/core/runtime/AvenxComponent.js @@ -436,6 +436,8 @@ export class AvenxComponent { this.#triggerLifecycle('onBeforeUpdate'); } + this.__updateProvidedState(); + this.#patcher.patch(this.#element, this.render(), (expression, slotScope) => this.#resolveTemplateExpression(expression, slotScope), this.$app); // Fill slots with transcluded content. @@ -454,6 +456,8 @@ export class AvenxComponent { if (this.#isMounted) { this.#triggerLifecycle('onUpdate'); } + + this.__notifyInjectingChildren(); } finally { this.#isUpdating = false; } @@ -1155,7 +1159,7 @@ export class AvenxComponent { // Create a reactive proxy of the provided object const handlerFactory = new ProxyHandlerFactory({ onChange: () => { - // Do not call scheduleUpdate of this component + this.__notifyInjectingChildren(); }, }); this._providedState = new Proxy(resolved, handlerFactory.create()); @@ -1193,6 +1197,39 @@ export class AvenxComponent { } } + /** + * Notifies all registered injecting child components that a provided value has shifted. + */ + __notifyInjectingChildren() { + if (this._injectingChildren) { + for (const children of this._injectingChildren.values()) { + for (const child of children) { + child.scheduleUpdate(); + } + } + } + } + + /** + * Dynamically re-evaluates the provide option and updates providedState. + */ + __updateProvidedState() { + if (!this._providedState) return; + const provideOption = + this.provide || + (typeof this.constructor.provide === 'function' ? this.constructor.provide() : this.constructor.provide); + if (!provideOption) return; + + const resolved = typeof provideOption === 'function' ? provideOption.call(this) : provideOption; + if (resolved && typeof resolved === 'object' && !Array.isArray(resolved)) { + for (const [k, v] of Object.entries(resolved)) { + if (this._providedState[k] !== v) { + this._providedState[k] = v; + } + } + } + } + /** * Retrieves a property or method from the component's scope. * Used for array-based provide to resolve keys dynamically. From 7b57dede1535e89db3a66728d29a3dd238929f94 Mon Sep 17 00:00:00 2001 From: nathanschmid08 Date: Wed, 29 Jul 2026 20:41:46 +0200 Subject: [PATCH 3/4] test(reactivity): add reactive provide/inject integration test for parent state primitive reassignment --- test/integration/provide_inject.test.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/integration/provide_inject.test.js b/test/integration/provide_inject.test.js index af1c993..347be9a 100644 --- a/test/integration/provide_inject.test.js +++ b/test/integration/provide_inject.test.js @@ -627,6 +627,71 @@ global.requestAnimationFrame = (cb) => { console.log(' ✅ Nested scopes and shadowing tests passed!'); + // ------------------------------------------------------------------------- + // Test Case 4: Reactive Provide/Inject Dependency Updates (Primitive Reassignment) + // ------------------------------------------------------------------------- + console.log(' 4. Testing reactive provide / inject dependency updates with primitive reassignment...'); + + let reactiveChildRenders = 0; + + class ReactiveChildComp extends AvenxComponent { + constructor(bridges, props) { + super({}, {}, bridges, '
Child Theme: {{ theme }}
', {}, props); + } + runUpdate() { + reactiveChildRenders++; + super.runUpdate(); + } + } + ReactiveChildComp.inject = ['theme']; + + class ReactiveParentPage extends AvenxPage { + constructor(bridges, registry) { + super( + { parentTheme: 'initial-dark' }, + {}, + bridges, + '
' + ' ' + '
', + {}, + registry, + ); + } + provide() { + return { + theme: this.state.parentTheme, + }; + } + } + + const registry4 = new Map(); + registry4.set('ReactiveChildComp', ReactiveChildComp); + + testRootElement.innerHTML = ''; + const parentPage4 = new ReactiveParentPage({}, registry4); + parentPage4.mount(testRootElement); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const childEl4 = testRootElement.querySelector('[data-avenx-comp="ReactiveChildComp"]'); + assert.ok(childEl4, 'Child component should be mounted in the DOM'); + const childInstance4 = childEl4.__avenx_comp_instance; + assert.ok(childInstance4, 'Child component instance should be cached on element'); + + assert.strictEqual(childInstance4.theme, 'initial-dark', 'Child should inject initial theme'); + assert.strictEqual(childEl4.textContent.trim(), 'Child Theme: initial-dark'); + assert.strictEqual(reactiveChildRenders, 1, 'Child should have rendered once'); + + // Mutate parent state. Since parentTheme shifts, it should trigger child update check + parentPage4.state.parentTheme = 'new-light'; + + await new Promise((resolve) => setTimeout(resolve, 50)); + + assert.strictEqual(childInstance4.theme, 'new-light', 'Child injected theme should update reactively'); + assert.strictEqual(childEl4.textContent.trim(), 'Child Theme: new-light', 'Child template should render updated value'); + assert.strictEqual(reactiveChildRenders, 2, 'Child component should have rendered a second time'); + + console.log(' ✅ Reactive Provide/Inject updates tests passed!'); + console.log('✅ Provide / Inject Integration Tests successfully completed!'); process.exit(0); } catch (error) { From 71a1fc0aea2135a6061ecdb2666e8d810d8ec0c3 Mon Sep 17 00:00:00 2001 From: nathanschmid08 Date: Wed, 29 Jul 2026 20:42:33 +0200 Subject: [PATCH 4/4] feat(reactivity): bypass proxy tracking in parent __updateProvidedState to prevent self-dependency loops --- lib/core/runtime/AvenxComponent.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/runtime/AvenxComponent.js b/lib/core/runtime/AvenxComponent.js index d9a262e..766b367 100644 --- a/lib/core/runtime/AvenxComponent.js +++ b/lib/core/runtime/AvenxComponent.js @@ -14,7 +14,7 @@ import { queueJob, nextTick as schedulerNextTick } from '../reactive/scheduler.j import { AvenxSandbox } from '../security/sandbox.js'; import { AvenxWatcher } from '../reactive/watcher.js'; -import { ProxyHandlerFactory } from '../reactive/proxyHandler.js'; +import { ProxyHandlerFactory, RAW_SYMBOL } from '../reactive/proxyHandler.js'; import { processBindDirectives } from '../utils/templateUtils.js'; import { profile } from '../utils/profiler.js'; @@ -1222,8 +1222,9 @@ export class AvenxComponent { const resolved = typeof provideOption === 'function' ? provideOption.call(this) : provideOption; if (resolved && typeof resolved === 'object' && !Array.isArray(resolved)) { + const rawProvided = this._providedState[RAW_SYMBOL] || this._providedState; for (const [k, v] of Object.entries(resolved)) { - if (this._providedState[k] !== v) { + if (rawProvided[k] !== v) { this._providedState[k] = v; } }