diff --git a/etaf-runtime.el b/etaf-runtime.el index c83b6f2..7e30547 100644 --- a/etaf-runtime.el +++ b/etaf-runtime.el @@ -3460,10 +3460,20 @@ The candidate uses resolved VALUE, DEPS, and NODES." (car output-range)))) (etaf-runtime-candidate-artifacts runtime)) (push identity (etaf-runtime-candidate-rendered-identities runtime)) - (list candidate - (gethash (etaf--semantic-component-artifact-key committed) - (etaf-runtime-artifact-registry runtime)) - (gethash effect-id (etaf-runtime-candidate-artifacts runtime)))))) + (let ((committed-artifact + (and (etaf--semantic-component-artifact-key committed) + (gethash (etaf--semantic-component-artifact-key committed) + (etaf-runtime-artifact-registry runtime))))) + ;; Descendant-only publications invalidate cached ancestor artifacts + ;; while retaining the ancestor semantic tree. When that ancestor + ;; becomes dirty on a later turn, rebuild its committed backend anchor + ;; from the immutable generation before diffing the new render. + (unless committed-artifact + (setq committed-artifact + (etaf--runtime-rebuild-component-artifact runtime committed))) + (list candidate committed-artifact + (gethash effect-id + (etaf-runtime-candidate-artifacts runtime))))))) (defun etaf--runtime-render-dirty-range (runtime effect range) "Evaluate RUNTIME dirty RANGE EFFECT without running its Component owner." diff --git a/tests/etaf-tests.el b/tests/etaf-tests.el index f6d7045..39284cd 100644 --- a/tests/etaf-tests.el +++ b/tests/etaf-tests.el @@ -86,6 +86,8 @@ (defvar etaf-test-transparent-parent-renders 0) (defvar etaf-test-transparent-inner-renders 0) (defvar etaf-test-transparent-outer-renders 0) +(defvar etaf-test-ancestor-parent-source nil) +(defvar etaf-test-ancestor-child-source nil) (defvar etaf-test-context-provider-source nil) (defvar etaf-test-context-provider-renders 0) (defvar etaf-test-context-consumer-renders 0) @@ -594,6 +596,19 @@ "Place a transparent Component chain in one material parent." :view (column (etaf-test-transparent-outer))) +(etaf-define-component etaf-test-ancestor-artifact-child () + "Render a child whose local update invalidates ancestor artifacts." + :view + (text (expr :value + (if (etaf-value etaf-test-ancestor-child-source) + "detail" "child")))) + +(etaf-define-component etaf-test-ancestor-artifact-parent () + "Render a parent that can update after one descendant-only publication." + :view + (column :bgcolor (etaf-value etaf-test-ancestor-parent-source) + (etaf-test-ancestor-artifact-child))) + (etaf-define-component etaf-test-generation-context-consumer () "Render one generation-owned Context dependency." :setup @@ -3813,6 +3828,35 @@ Event composition is a Runtime contract, not a UI-library helper contract." (etaf-unmount runtime)) (when-let ((buffer (get-buffer buffer-name))) (kill-buffer buffer))))) +(ert-deftest etaf-runtime-parent-rerender-rebuilds-invalidated-artifact () + "Rebuild a committed parent anchor after a descendant-only publication." + (let ((buffer-name " *etaf-ancestor-artifact-test*") + (etaf-test-ancestor-parent-source (etaf-ref "light")) + (etaf-test-ancestor-child-source (etaf-ref nil))) + (unwind-protect + (progn + (etaf-mount buffer-name + (etaf-view (etaf-test-ancestor-artifact-parent))) + (setf (etaf-value etaf-test-ancestor-child-source) t) + (let* ((runtime (etaf-runtime-for-buffer buffer-name)) + (generation (etaf-runtime-current-generation runtime)) + (parent + (etaf--generation-semantic + generation '(etaf-test-ancestor-artifact-parent (root))))) + (should (etaf--semantic-component-p parent)) + (should-not (etaf--semantic-component-artifact-key parent)) + (setf (etaf-value etaf-test-ancestor-parent-source) "dark") + (should (equal + (plist-get + (etaf-runtime-host-props-for + runtime '(etaf-host (root :view))) + :bgcolor) + "dark")))) + (when-let ((runtime (etaf-runtime-for-buffer buffer-name))) + (etaf-unmount runtime)) + (when-let ((buffer (get-buffer buffer-name))) + (kill-buffer buffer))))) + (ert-deftest etaf-runtime-context-provider-promotes-consumer-atomically () "Promote Context provider/consumer frames only with their generation." (let ((buffer-name " *etaf-generation-context-test*")