🐛 Fix problems with react loops (#11941)

This commit is contained in:
Alonso Torres 2026-09-28 16:59:06 +02:00 committed by GitHub
parent b890b94d27
commit c0714def01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 87 deletions

View File

@ -1473,12 +1473,17 @@
(rx/filter (complement :skip-component-sync?)) (rx/filter (complement :skip-component-sync?))
;; Keep waits pending while component changes are checked. ;; Keep waits pending while component changes are checked.
(rx/map start-sync-barrier) (rx/map start-sync-barrier)
(rx/observe-on :async)) (rx/share))
get-component-events ;; Buffers commits until a timer turn passes with no new ones. The
(fn [[event old-data]] ;; debounce timer (not a microtask) queues behind any timers already
(let [{:keys [file-id changes save-undo? undo-group]} event ;; due, so a burst of commits is inspected as one batch in one task.
changed-components commit-batches-s
(rx/buffer-until (rx/debounce 0 commits-s) commits-s)
get-component-changes
(fn [old-data {:keys [file-id changes save-undo? undo-group]}]
(let [changed-components
(when (and old-data (when (and old-data
(or (nil? file-id) (= file-id (:id old-data)))) (or (nil? file-id) (= file-id (:id old-data))))
(into #{} (into #{}
@ -1486,29 +1491,39 @@
changes))] changes))]
(cond (cond
(empty? changed-components) (empty? changed-components)
(rx/empty) nil
save-undo? save-undo?
(do (do
(log/info :hint "detected component changes" (log/info :hint "detected component changes"
:ids (map str changed-components) :ids (map str changed-components)
:undo-group undo-group) :undo-group undo-group)
(->> (rx/from changed-components) (map #(vector ::component-changed % undo-group)
(rx/map #(component-changed changed-components))
% (:id old-data) undo-group))))
:else :else
;; Undos only bump :modified-at. ;; Undos only bump :modified-at.
(->> (rx/from changed-components) (map #(vector ::touch-component %) changed-components))))
(rx/map touch-component)))))
;; One event per distinct component change in the batch
get-component-events
(fn [batch old-data]
(->> batch
(mapcat #(get-component-changes old-data (first %)))
(distinct)
(map (fn [[type component-id undo-group]]
(if (= type ::component-changed)
(component-changed component-id (:id old-data) undo-group)
(touch-component component-id))))
(rx/from)))
component-events-s component-events-s
(->> commits-s (->> commit-batches-s
(rx/with-latest-from workspace-buffer-s) (rx/with-latest-from workspace-buffer-s)
(rx/mapcat (rx/mapcat
(fn [[[event task] old-data]] (fn [[batch old-data]]
(->> (get-component-events [event old-data]) (->> (get-component-events batch old-data)
(rx/finalize #(finish-sync-barrier! task))))) (rx/finalize #(run! (comp finish-sync-barrier! second) batch)))))
;; Close barriers left behind when the page shuts down. ;; Close barriers left behind when the page shuts down.
(rx/finalize #(wrf/finish-tasks! @pending-sync-barriers*)) (rx/finalize #(wrf/finish-tasks! @pending-sync-barriers*))
(rx/share)) (rx/share))

View File

@ -202,12 +202,6 @@
file-id (mf/use-ctx ctx/current-file-id) file-id (mf/use-ctx ctx/current-file-id)
current-colors* (mf/use-state [])
current-colors (deref current-colors*)
grouped-colors* (mf/use-state {})
grouped-colors (deref grouped-colors*)
open-groups* (mf/use-state #{}) open-groups* (mf/use-state #{})
open-groups (deref open-groups*) open-groups (deref open-groups*)
@ -221,13 +215,15 @@
[{:value "recent" :label (tr "workspace.libraries.colors.recent-colors") :id "recent"} [{:value "recent" :label (tr "workspace.libraries.colors.recent-colors") :id "recent"}
{:value "file" :label (tr "workspace.libraries.colors.file-library") :id "file"}]) {:value "file" :label (tr "workspace.libraries.colors.file-library") :id "file"}])
;; Equal-stable, since `select*` resets its selection when options change
options options
(mf/with-memo [library-options libraries file-id] (h/use-equal-memo
(into library-options (mf/with-memo [library-options libraries file-id]
(comp (into library-options
(map val) (comp
(map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))}))) (map val)
(dissoc libraries file-id))) (map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))})))
(dissoc libraries file-id))))
on-library-change on-library-change
(mf/use-fn (mf/use-fn
@ -281,49 +277,49 @@
(fn [s] (fn [s]
(if (contains? s path) (if (contains? s path)
(disj s path) (disj s path)
(conj s path))))))] (conj s path))))))
;; Load library colors when the selected library (or filter options) change. resolved-file-id
;; (if (= selected :file) file-id selected)
;; flat current-colors* -- used for the grid view and the recent list view.
;; grouped grouped-colors* -- used for the library grouped list view.
;;
;; Library colors are fully converted with `library-color->color` here so
;; the render path never needs to do it. `flat-colors` is materialised as
;; an eager vector so realisation does not leak into render time.
;; open-groups* is reset to #{} (all groups expanded) on every library switch.
(mf/with-effect [selected recent-colors libraries file-id valid-color?]
(let [resolved-file-id (if (= selected :file) file-id selected)]
(reset! open-groups* #{})
(if (= selected :recent)
(let [colors (into []
(comp
(filter valid-color?)
(map-indexed (fn [index color]
(let [color (if (map? color) color {:color color})]
(vary-meta color assoc ::id (dm/str index)))))
(take-while some?))
(sort ctc/sort-colors (reverse recent-colors)))]
(reset! current-colors* colors)
(reset! grouped-colors* {}))
(let [raw-colors (->> (dm/get-in libraries [resolved-file-id :data :colors]) ;; Colors of the selected library; stable across unrelated commits
(vals) library-colors
(filter valid-color?) (when (not= selected :recent)
(sort-by :name)) (dm/get-in libraries [resolved-file-id :data :colors]))
;; Eager vector for the grid view -- index-based ::id for keying. ;; Flat (grid and recent list) and grouped (library list) colors, converted for rendering
flat-colors (into [] [current-colors grouped-colors]
(map-indexed (fn [index color] (mf/with-memo [selected recent-colors library-colors resolved-file-id valid-color?]
(-> (ctc/library-color->color color resolved-file-id) (if (= selected :recent)
(vary-meta assoc ::id (dm/str index))))) [(into []
raw-colors) (comp
(filter valid-color?)
(map-indexed (fn [index color]
(let [color (if (map? color) color {:color color})]
(vary-meta color assoc ::id (dm/str index)))))
(take-while some?))
(sort ctc/sort-colors (reverse recent-colors)))
{}]
;; Group tree with colors already converted -- no conversions at render time. (let [raw-colors (->> (vals library-colors)
grouped (some-> (grp/group-assets raw-colors false) (filter valid-color?)
(convert-grouped-colors resolved-file-id))] (sort-by :name))
(reset! current-colors* flat-colors)
(reset! grouped-colors* (or grouped {})))))) ;; Eager vector for the grid view -- index-based ::id for keying.
flat-colors (into []
(map-indexed (fn [index color]
(-> (ctc/library-color->color color resolved-file-id)
(vary-meta assoc ::id (dm/str index)))))
raw-colors)
;; Group tree with colors already converted -- no conversions at render time.
grouped (some-> (grp/group-assets raw-colors false)
(convert-grouped-colors resolved-file-id))]
[flat-colors (or grouped {})])))]
;; Expands all groups when the selected library changes
(mf/with-effect [selected]
(reset! open-groups* #{}))
[:div {:class (stl/css :libraries)} [:div {:class (stl/css :libraries)}
[:div {:class (stl/css :select-wrapper)} [:div {:class (stl/css :select-wrapper)}

View File

@ -121,10 +121,15 @@
(reset! internal-color* color) (reset! internal-color* color)
(on-change color))))] (on-change color))))]
;; Syncs with color changes made outside the ramp; the colors the ramp
;; emits come back with the same components and are skipped
(mf/use-effect (mf/use-effect
(mf/deps color) (mf/deps color)
(fn [] (fn []
(reset! internal-color* (enrich-color-map color)))) (let [color (enrich-color-map color)]
(when (not= (select-keys color [:h :s :v :alpha])
(select-keys internal-color [:h :s :v :alpha]))
(reset! internal-color* color)))))
[:* [:*
[:> value-saturation-selector* [:> value-saturation-selector*

View File

@ -100,6 +100,21 @@
(when (= (:type panel) :component-swap) (when (= (:type panel) :component-swap)
[:> component-menu* {:shapes (:shapes panel) :is-swap-opened true}])) [:> component-menu* {:shapes (:shapes panel) :is-swap-opened true}]))
(defn- get-shapes-with-children
"Returns the shapes of `selected` together with all their descendants."
[objects selected]
(loop [queue (into #queue [] selected)
visited selected]
(if-let [id (peek queue)]
(let [shape (get objects id)
children (:shapes shape)]
(if (seq children)
(let [new-children (remove visited children)]
(recur (into (pop queue) new-children)
(into visited new-children)))
(recur (pop queue) visited)))
(sequence (keep (d/getf objects)) visited))))
(mf/defc design-menu* (mf/defc design-menu*
{::mf/private true} {::mf/private true}
[{:keys [selected objects page-id file-id shapes]}] [{:keys [selected objects page-id file-id shapes]}]
@ -145,29 +160,16 @@
(->> (dm/get-in grid-edition [edition :selected]) (->> (dm/get-in grid-edition [edition :selected])
(map #(dm/get-in objects [edition :layout-grid-cells %]))) (map #(dm/get-in objects [edition :layout-grid-cells %])))
shapes-with-children* ;; Deferred, so the subtree walk runs in a background render
(mf/use-state nil) deferred-selected
(mf/use-deferred selected)
_ (mf/use-effect deferred-objects
(mf/deps selected objects shapes) (mf/use-deferred objects)
(fn []
(reset! shapes-with-children* nil)
(let [result
(loop [queue (into #queue [] selected)
visited selected]
(if-let [id (peek queue)]
(let [shape (get objects id)
children (:shapes shape)]
(if (seq children)
(let [new-children (remove visited children)]
(recur (into (pop queue) new-children)
(into visited new-children)))
(recur (pop queue) visited)))
(sequence (keep (d/getf objects)) visited)))]
(reset! shapes-with-children* result))))
shapes-with-children shapes-with-children
(deref shapes-with-children*) (mf/with-memo [deferred-selected deferred-objects]
(get-shapes-with-children deferred-objects deferred-selected))
total-selected total-selected
(count selected)] (count selected)]