🐛 Prevent thumbnail frame recursion overflow (#8763)

Cache in-progress frame traversals before following parent frame links so thumbnail updates stop recursing forever on cyclic or transiently inconsistent shape graphs.

Add a regression test that covers cyclic frame-id chains and keeps the expected frame/component extraction behavior intact.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh 2026-04-07 15:09:54 +02:00 committed by GitHub
parent 2ca7acfca6
commit b99157a246
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 18 deletions

View File

@ -217,31 +217,37 @@
root-frame-old? (cfh/root-frame? old-objects old-frame-id)
root-frame-new? (cfh/root-frame? new-objects new-frame-id)
instance-root? (ctc/instance-root? new-shape)]
instance-root? (ctc/instance-root? new-shape)
local-result (cond-> #{}
root-frame-old?
(conj ["frame" old-frame-id])
(cond-> #{}
root-frame-old?
(conj ["frame" old-frame-id])
root-frame-new?
(conj ["frame" new-frame-id])
root-frame-new?
(conj ["frame" new-frame-id])
instance-root?
(conj ["component" id]))]
instance-root?
(conj ["component" id])
(swap! frame-id-cache assoc id {:status :in-progress
:result local-result})
(and (uuid? (:frame-id old-shape))
(not= uuid/zero (:frame-id old-shape)))
(into (get-frame-ids (:frame-id old-shape)))
(let [result
(cond-> local-result
(and (uuid? (:frame-id old-shape))
(not= uuid/zero (:frame-id old-shape)))
(into (get-frame-ids-cached (:frame-id old-shape)))
(and (uuid? (:frame-id new-shape))
(not= uuid/zero (:frame-id new-shape)))
(into (get-frame-ids (:frame-id new-shape))))))
(and (uuid? (:frame-id new-shape))
(not= uuid/zero (:frame-id new-shape)))
(into (get-frame-ids-cached (:frame-id new-shape))))]
(swap! frame-id-cache assoc id {:status :done
:result result})
result)))
(get-frame-ids-cached [id]
(or (get @frame-id-cache id)
(let [result (get-frame-ids id)]
(swap! frame-id-cache assoc id result)
result)))]
(if-let [cached (get @frame-id-cache id)]
(:result cached)
(get-frame-ids id)))]
(into #{}
(comp (mapcat extract-ids)
(filter (fn [[page-id']] (= page-id page-id')))

View File

@ -0,0 +1,36 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.data.workspace-thumbnails-test
(:require
[app.common.uuid :as uuid]
[app.main.data.workspace.thumbnails :as thumbnails]
[cljs.test :as t :include-macros true]))
(t/deftest extract-frame-changes-handles-cyclic-frame-links
(let [page-id (uuid/next)
root-id (uuid/next)
shape-a-id (uuid/next)
shape-b-id (uuid/next)
event {:changes [{:type :mod-obj
:page-id page-id
:id shape-a-id}]}
old-data {:pages-index
{page-id
{:objects
{root-id {:id root-id :type :frame :frame-id uuid/zero}
shape-a-id {:id shape-a-id :type :rect :frame-id shape-b-id}
shape-b-id {:id shape-b-id :type :group :frame-id shape-a-id}}}}}
new-data {:pages-index
{page-id
{:objects
{root-id {:id root-id :type :frame :frame-id uuid/zero}
shape-a-id {:id shape-a-id :type :rect :frame-id root-id}
shape-b-id {:id shape-b-id :type :group :frame-id shape-a-id
:component-root true}}}}}]
(t/is (= #{["frame" root-id]
["component" shape-b-id]}
(#'thumbnails/extract-frame-changes page-id [event [old-data new-data]])))))

View File

@ -6,6 +6,7 @@
[frontend-tests.data.viewer-test]
[frontend-tests.data.workspace-colors-test]
[frontend-tests.data.workspace-texts-test]
[frontend-tests.data.workspace-thumbnails-test]
[frontend-tests.helpers-shapes-test]
[frontend-tests.logic.comp-remove-swap-slots-test]
[frontend-tests.logic.components-and-tokens]
@ -43,6 +44,7 @@
'frontend-tests.data.viewer-test
'frontend-tests.data.workspace-colors-test
'frontend-tests.data.workspace-texts-test
'frontend-tests.data.workspace-thumbnails-test
'frontend-tests.helpers-shapes-test
'frontend-tests.logic.comp-remove-swap-slots-test
'frontend-tests.logic.components-and-tokens