mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 17:40:39 +00:00
Emit changes-applied with the commit's prior file data. Sync each commit before a later edit can replace its state or separate its undo group. Allow later commits through while completion barriers finish (F110). Process layout batches after their buffer closes, so reentrant component sync cannot lose a pending layout task. Cover three user edits without an intermediate wait, then every undo and redo. Token and tiny-layout cases also pass. Re-record golden cases X and AJ for the settled timing. Frontend: 1495 tests, 7255 assertions pass; lint and format checks pass. The three-edit regression failed before the fix. Benchmark vs Phase 1e (lower quantile, ms): world baseline now ratio main-10 1.790 0.993 0.55 main-100 15.415 9.038 0.59 main-1000 153.086 90.145 0.59 three-levels 35.215 18.989 0.54 chain 68.931 35.045 0.51 library-500 83.083 85.830 1.03 many-shapes 4.837 3.344 0.69 variant-100 32.052 33.586 1.05 All worlds pass the 10% regression threshold. AI-assisted-by: GPT-6
279 lines
10 KiB
Clojure
279 lines
10 KiB
Clojure
;; 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 SUBSIDIARY SL
|
|
|
|
(ns app.main.data.changes
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.changes :as cpc]
|
|
[app.common.logging :as log]
|
|
[app.common.time :as ct]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.shape-tree :as ctst]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.features :as features]
|
|
[app.main.worker :as mw]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.render-wasm.shape :as wasm.shape]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
;; Change this to :info :debug or :trace to debug this module
|
|
(log/set-level! :warn)
|
|
|
|
(def page-change?
|
|
#{:add-page :mod-page :del-page :mov-page})
|
|
|
|
(def update-layout-attr?
|
|
#{:hidden})
|
|
|
|
(def commit?
|
|
(ptk/type? ::commit))
|
|
|
|
(def changes-applied?
|
|
(ptk/type? ::changes-applied))
|
|
|
|
(defn- fix-page-id
|
|
"For events that modifies the page, page-id does not comes
|
|
as a property so we assign it from the `id` property."
|
|
[{:keys [id type page] :as change}]
|
|
(cond-> change
|
|
(and (page-change? type)
|
|
(nil? (:page-id change)))
|
|
(assoc :page-id (or id (:id page)))))
|
|
|
|
(defn- update-indexes
|
|
"Given a commit, send the changes to the worker for updating the
|
|
indexes."
|
|
[commit attr]
|
|
(ptk/reify ::update-indexes
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [changes (->> (get commit attr)
|
|
(map fix-page-id)
|
|
(filter :page-id)
|
|
(group-by :page-id))]
|
|
|
|
(->> (rx/from changes)
|
|
(rx/merge-map (fn [[page-id changes]]
|
|
(log/debug :hint "update-indexes" :page-id page-id :changes (count changes))
|
|
(mw/ask! {:cmd :index/update
|
|
:page-id page-id
|
|
:changes changes})))
|
|
(rx/catch (fn [cause]
|
|
(log/warn :hint "unable to update index"
|
|
:cause cause)
|
|
(rx/empty)))
|
|
(rx/ignore))))))
|
|
|
|
(defn- get-pending-commits
|
|
[{:keys [persistence]}]
|
|
(->> (:queue persistence)
|
|
(map (d/getf (:index persistence)))
|
|
(not-empty)))
|
|
|
|
(def ^:private xf:map-page-id
|
|
(keep :page-id))
|
|
|
|
(def ^:private wasm-structural-change-types
|
|
#{:add-obj :mov-objects})
|
|
|
|
(defn- redo-changes-need-wasm-object-sync?
|
|
[redo-changes]
|
|
(some #(contains? wasm-structural-change-types (:type %)) redo-changes))
|
|
|
|
(defn- sync-wasm-structural-changes
|
|
[{:keys [redo-changes]}]
|
|
(ptk/reify ::sync-wasm-structural-changes
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(when (wasm.api/initialized?)
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shapes
|
|
(into []
|
|
(keep (fn [{:keys [type id parent-id]}]
|
|
(when (contains? wasm-structural-change-types type)
|
|
(get objects (case type
|
|
:add-obj id
|
|
:mov-objects parent-id)))))
|
|
redo-changes)]
|
|
|
|
(wasm.api/process-objects shapes)
|
|
(wasm.api/request-render "sync-wasm-structural-changes"))))))
|
|
|
|
(defn- apply-changes-localy
|
|
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
|
(ptk/reify ::apply-changes-localy
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [undo-changes
|
|
(if pending
|
|
(->> pending
|
|
(map :undo-changes)
|
|
(reverse)
|
|
(mapcat identity)
|
|
(vec))
|
|
nil)
|
|
|
|
redo-changes
|
|
(if pending
|
|
(into redo-changes
|
|
(mapcat :redo-changes)
|
|
pending)
|
|
redo-changes)
|
|
|
|
apply-changes
|
|
(fn [fdata]
|
|
(let [fdata (cpc/process-changes fdata undo-changes false)
|
|
fdata (cpc/process-changes fdata redo-changes false)
|
|
pids (into #{} xf:map-page-id redo-changes)]
|
|
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
|
|
|
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
|
;; Update the wasm model
|
|
(let [shape-changes (js/Map.)
|
|
|
|
state
|
|
(binding [cts/*shape-changes* shape-changes]
|
|
(update-in state [:files file-id :data] apply-changes))]
|
|
|
|
(let [objects (dm/get-in state [:files file-id :data :pages-index (:current-page-id state) :objects])]
|
|
(wasm.shape/process-shape-changes! objects (es6-iterator-seq (.entries shape-changes))))
|
|
|
|
state)
|
|
|
|
;; wasm renderer deactivated
|
|
(update-in state [:files file-id :data] apply-changes))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
;; `:add-obj` / `:mov-objects` are not tracked via `*shape-changes*`.
|
|
;; Emit only for structural commits, after file data is in state.
|
|
(when (and (not ignore-wasm?)
|
|
(features/active-feature? state "render-wasm/v1")
|
|
(redo-changes-need-wasm-object-sync? redo-changes))
|
|
(rx/of (sync-wasm-structural-changes {:redo-changes redo-changes}))))))
|
|
|
|
(defn commit
|
|
"Create a commit event instance"
|
|
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
|
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?
|
|
selected-before translation? skip-component-sync?]}]
|
|
|
|
(assert (cpc/check-changes redo-changes)
|
|
"expect valid vector of changes for redo-changes")
|
|
|
|
(assert (cpc/check-changes undo-changes)
|
|
"expect valid vector of changes for undo-changes")
|
|
|
|
(let [commit-id (or commit-id (uuid/next))
|
|
source (d/nilv source :local)
|
|
local? (= source :local)
|
|
commit {:id commit-id
|
|
:created-at (ct/now)
|
|
:source source
|
|
:origin (ptk/type origin)
|
|
:features features
|
|
:file-id file-id
|
|
:file-revn file-revn
|
|
:file-vern file-vern
|
|
:changes redo-changes
|
|
:redo-changes redo-changes
|
|
:undo-changes undo-changes
|
|
:save-undo? save-undo?
|
|
:undo-group undo-group
|
|
:tags tags
|
|
:stack-undo? stack-undo?
|
|
:ignore-wasm? ignore-wasm?
|
|
:selected-before selected-before
|
|
:translation? translation?
|
|
:skip-component-sync? skip-component-sync?}]
|
|
|
|
(ptk/reify ::commit
|
|
cljs.core/IDeref
|
|
(-deref [_] commit)
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [pending (when-not local?
|
|
(get-pending-commits state))]
|
|
(rx/concat
|
|
(rx/of (apply-changes-localy commit pending))
|
|
(rx/of (ptk/data-event ::changes-applied
|
|
{:commit commit
|
|
:old-data (get-in state [:files file-id :data])}))
|
|
(if pending
|
|
(rx/concat
|
|
(->> (rx/from (reverse pending))
|
|
(rx/map (fn [commit] (update-indexes commit :undo-changes))))
|
|
(rx/of (update-indexes commit :redo-changes))
|
|
(->> (rx/from pending)
|
|
(rx/map (fn [commit] (update-indexes commit :redo-changes)))))
|
|
(rx/of (update-indexes commit :redo-changes)))))))))
|
|
|
|
(defn- resolve-file-revn
|
|
[state file-id]
|
|
(:revn (dsh/lookup-file state file-id)))
|
|
|
|
(defn- resolve-file-vern
|
|
[state file-id]
|
|
(:vern (dsh/lookup-file state file-id)))
|
|
|
|
(defn commit-changes
|
|
"Schedules a list of changes to execute now, and add the corresponding undo changes to
|
|
the undo stack.
|
|
|
|
Options:
|
|
- save-undo?: if set to false, do not add undo changes.
|
|
- undo-group: if some consecutive changes (or even transactions) share the same
|
|
undo-group, they will be undone or redone in a single step
|
|
"
|
|
[{:keys [redo-changes undo-changes save-undo? undo-group tags stack-undo? file-id
|
|
translation? skip-component-sync?]
|
|
:or {save-undo? true
|
|
stack-undo? false
|
|
undo-group (uuid/next)
|
|
tags #{}}
|
|
:as params}]
|
|
(ptk/reify ::commit-changes
|
|
ev/PerformanceEvent
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (or file-id (:current-file-id state))
|
|
uchg (vec undo-changes)
|
|
rchg (vec redo-changes)
|
|
features (get state :features)
|
|
permissions (get state :permissions)]
|
|
|
|
;; Historical previews must not create edits to the live file. Check
|
|
;; this when creating commits so previously queued edits can still save.
|
|
;; Refusals answer with an empty stream (never nil) so callers can
|
|
;; uniformly subscribe and observe termination.
|
|
(if (and (:can-edit permissions)
|
|
(not (dm/get-in state [:workspace-global :preview-id])))
|
|
(do
|
|
(log/trace :hint "commit-changes" :redo-changes redo-changes)
|
|
(let [selected (dm/get-in state [:workspace-local :selected])]
|
|
(rx/of (-> params
|
|
(assoc :undo-group undo-group)
|
|
(assoc :features features)
|
|
(assoc :tags tags)
|
|
(assoc :stack-undo? stack-undo?)
|
|
(assoc :save-undo? save-undo?)
|
|
(assoc :file-id file-id)
|
|
(assoc :file-revn (resolve-file-revn state file-id))
|
|
(assoc :file-vern (resolve-file-vern state file-id))
|
|
(assoc :undo-changes uchg)
|
|
(assoc :redo-changes rchg)
|
|
(assoc :selected-before selected)
|
|
(assoc :translation? translation?)
|
|
(assoc :skip-component-sync? skip-component-sync?)
|
|
(commit)))))
|
|
(rx/empty))))))
|