🐛 Fix incorrect handlig of version restore operation (#9041)

- Add session ID tracking to RPC layer (backend and frontend)
- Send session ID header with RPC requests for request correlation
- Rename file-restore to file-restored for consistency
- Extract initialize-file function from initialize-workspace flow
- Improve file restoration initialization with wait-for-persistence
- Extract initialize-version event handler for version restoration
- Fix viewport key generation with file version numbers for proper re-renders
- Update layout item schema and constraints to use internal sizing state
- Add v-sizing state retrieval in layout-size-constraints component
- Refactor file-change notifications stream handling with rx/map
- Fix team-id lookup in restore-version-from-plugins

Improves request traceability across frontend/backend sessions and streamlines
the workspace initialization flow for file restoration scenarios.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh 2026-04-21 19:19:51 +02:00
parent 8f2c467b82
commit a395768987
12 changed files with 114 additions and 61 deletions

View File

@ -92,6 +92,7 @@
(fn [{:keys [params path-params method] :as request}]
(let [handler-name (:method-name path-params)
etag (yreq/get-header request "if-none-match")
session-id (yreq/get-header request "x-session-id")
key-id (get request ::http/auth-key-id)
profile-id (or (::session/profile-id request)
@ -104,6 +105,7 @@
(assoc ::handler-name handler-name)
(assoc ::ip-addr ip-addr)
(assoc ::request-at (ct/now))
(assoc ::session-id (some-> session-id uuid/parse*))
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)
(assoc ::profile-id profile-id)))

View File

@ -71,7 +71,7 @@
{::doc/added "1.20"
::sm/params schema:restore-file-snapshot
::db/transaction true}
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}]
(files/check-edition-permissions! conn profile-id file-id)
(let [file (bfc/get-file cfg file-id)
team (teams/get-team conn
@ -88,7 +88,8 @@
;; Send to the clients a notification to reload the file
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restore
:message {:type :file-restored
:session-id session-id
:file-id (:id file)
:vern vern})
nil)))

View File

@ -12,6 +12,7 @@
[app.common.logging :as log]
[app.common.time :as ct]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.common.version :as v]
[app.util.avatars :as avatars]
[app.util.extends]
@ -108,10 +109,12 @@
(def target (parse-target global))
(def browser (parse-browser))
(def platform (parse-platform))
(def session-id (uuid/next))
(def version (parse-version global))
(def version-tag (obj/get global "penpotVersionTag"))
(defn stale-build?
"Returns true when the compiled JS was built with a different version
tag than the one present in the current index.html. This indicates

View File

@ -9,7 +9,6 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.types.objects-map]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.auth :as da]
[app.main.data.event :as ev]
@ -43,7 +42,8 @@
(log/inf :version (:full cf/version)
:asserts *assert*
:build-date cf/build-date
:public-uri (dm/str cf/public-uri))
:public-uri (dm/str cf/public-uri)
:session-id (str cf/session-id))
(log/inf :hint "enabled flags" :flags (str/join " " (map name cf/flags))))
(declare reinit)
@ -61,7 +61,7 @@
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc state :session-id (uuid/next)))
(assoc state :session-id cf/session-id))
ptk/WatchEvent
(watch [_ _ stream]

View File

@ -431,6 +431,7 @@
context (-> @context
(merge (:context event))
(assoc :session session*)
(assoc :session-id cf/session-id)
(assoc :external-session-id (cf/external-session-id))
(add-external-context-info)
(d/without-nils))]

View File

@ -224,6 +224,7 @@
IDeref
(-deref [_] bundle)
ptk/UpdateEvent
(update [_ state]
(-> state
@ -246,6 +247,7 @@
(rx/of (dws/select-shapes frames-id)
dwz/zoom-to-selected-shape)))))
;; FIXME: rename to `fetch-file`
(defn- fetch-bundle
"Multi-stage file bundle fetch coordinator"
[file-id features]
@ -283,6 +285,20 @@
(when shape
(wasm.api/process-object shape))))))
(defn initialize-file
[team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-file
ptk/WatchEvent
(watch [_ state _]
(let [features (features/get-enabled-features state team-id)]
(log/dbg :hint "initialize-file"
:team-id (dm/str team-id)
:file-id (dm/str file-id))
(rx/of (fetch-bundle file-id features))))))
(defn initialize-workspace
[team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
@ -337,7 +353,7 @@
;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features)))
(rx/of (initialize-file team-id file-id)))
(->> stream
(rx/filter (ptk/type? ::bundle-fetched))
@ -471,7 +487,6 @@
(rx/take-until stoper-s))
(rx/of (mcp/notify-other-tabs-disconnect)))))
ptk/EffectEvent
(effect [_ _ _]
(let [name (dm/str "workspace-" file-id)]

View File

@ -1294,9 +1294,10 @@
(watch [_ _ stream]
(let [stopper-s
(->> stream
(rx/filter #(or (= ::dwpg/finalize-page (ptk/type %))
(= ::watch-component-changes (ptk/type %)))))
(rx/map ptk/type)
(rx/filter (fn [event-type]
(or (= ::dwpg/finalize-page event-type)
(= ::watch-component-changes event-type)))))
workspace-data-s
(->> (rx/from-atom refs/workspace-data {:emit-current-value? true})
(rx/share))

View File

@ -40,7 +40,7 @@
(declare handle-pointer-update)
(declare handle-file-change)
(declare handle-file-deleted)
(declare handle-file-restore)
(declare handle-file-restored)
(declare handle-library-change)
(declare handle-pointer-send)
(declare handle-export-update)
@ -132,7 +132,7 @@
:pointer-update (handle-pointer-update msg)
:file-change (handle-file-change msg)
:file-deleted (handle-file-deleted msg)
:file-restore (handle-file-restore msg)
:file-restored (handle-file-restored msg)
:library-change (handle-library-change msg)
:notification (dc/handle-notification msg)
:team-role-change (handle-change-team-role msg)
@ -283,22 +283,22 @@
(rt/nav :dashboard-recent {:team-id team-id})))))))
(def ^:private
schema:handle-file-restore
[:map {:title "handle-file-restore"}
schema:handle-file-restored
[:map {:title "handle-file-restored"}
[:type :keyword]
[:file-id ::sm/uuid]
[:vern :int]])
(def ^:private check-file-restore-params
(sm/check-fn schema:handle-file-restore))
(def ^:private check-file-restored-params
(sm/check-fn schema:handle-file-restored))
(defn handle-file-restore
(defn handle-file-restored
[{:keys [file-id vern] :as msg}]
(assert (check-file-restore-params msg)
(assert (check-file-restored-params msg)
"expected valid parameters")
(ptk/reify ::handle-file-restore
(ptk/reify ::handle-file-restored
ptk/WatchEvent
(watch [_ state _]
(let [curr-file-id (:current-file-id state)

View File

@ -11,9 +11,10 @@
[app.common.schema :as sm]
[app.common.time :as ct]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.notifications :as ntf]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.thumbnails :as th]
[app.main.refs :as refs]
[app.main.repo :as rp]
@ -92,33 +93,59 @@
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
(rx/map fetch-versions)))))))
(defn- initialize-version
[]
(ptk/reify ::initialize-version
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
file-id (:current-file-id state)
team-id (:current-team-id state)]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dw/bundle-fetched))
(rx/take 1)
(rx/map #(dwpg/initialize-page file-id page-id)))
(rx/of (ntf/hide :tag :restore-dialog)
(dw/initialize-file team-id file-id)))))
ptk/EffectEvent
(effect [_ _ _]
(th/clear-queue!))))
(defn- wait-for-persistence
[file-id snapshot-id]
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
(defn restore-version
[id origin]
(assert (uuid? id) "expected valid uuid for `id`")
(ptk/reify ::restore-version
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
team-id (:current-team-id state)]
(let [file-id (:current-file-id state)
team-id (:current-team-id state)
event-name (case origin
:version "restore-pin-version"
:snapshot "restore-autosave"
:plugin "restore-version-plugin")]
(rx/concat
(rx/of ::dwp/force-persist
(dw/remove-layout-flag :document-history))
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace team-id file-id)))
(case origin
:version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
:snapshot
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
:plugin
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
(->> (wait-for-persistence file-id id)
(rx/map #(initialize-version)))
(if event-name
(rx/of (ev/event {::ev/name event-name
:file-id file-id
:team-id team-id}))
(rx/empty)))))))
(defn delete-version
@ -220,18 +247,15 @@
(ptk/reify ::restore-version-from-plugins
ptk/WatchEvent
(watch [_ state _]
(let [file (dsh/lookup-file state file-id)
team-id (or (:team-id file) (:current-file-id state))]
(let [team-id (:current-team-id state)]
(rx/concat
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
(rx/of (ev/event {::ev/name "restore-version-plugin"
:file-id file-id
:team-id team-id})
::dwp/force-persist)
;; FIXME: we should abstract this
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace team-id file-id)))
(->> (wait-for-persistence file-id id)
(rx/map #(initialize-version)))
(->> (rx/of 1)
(rx/tap resolve)

View File

@ -183,6 +183,7 @@
:credentials "include"
:headers {"accept" "application/transit+json,text/event-stream,*/*"
"x-external-session-id" (cf/external-session-id)
"x-session-id" (str cf/session-id)
"x-event-origin" (::ev/origin (meta params))}
:body (when (= method :post)
(if form-data?

View File

@ -539,16 +539,18 @@
[:map
[:values schema:layout-item-props-schema]
[:applied-tokens [:maybe [:map-of :keyword :string]]]
[:ids [::sm/vec ::sm/uuid]]
[:v-sizing {:optional true} [:maybe [:= :fill]]]])
[:ids [::sm/vec ::sm/uuid]]])
(mf/defc layout-size-constraints*
{::mf/private true
::mf/schema (sm/schema schema:layout-size-constraints)}
[{:keys [values v-sizing ids applied-tokens] :as props}]
[{:keys [values ids applied-tokens] :as props}]
(let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
v-sizing
(:layout-item-v-sizing values)
min-w (get values :layout-item-min-w)
max-w (get values :layout-item-max-w)
@ -914,5 +916,4 @@
(= v-sizing :fill))
[:> layout-size-constraints* {:ids ids
:values values
:applied-tokens applied-tokens
:v-sizing v-sizing}])])]))
:applied-tokens applied-tokens}])])]))

View File

@ -81,6 +81,7 @@
selected))
(mf/defc viewport-classic*
{::mf/private true}
[{:keys [selected wglobal layout file page palete-size]}]
(let [{:keys [edit-path
panning
@ -108,8 +109,8 @@
;; DEREFS
drawing (mf/deref refs/workspace-drawing)
focus (mf/deref refs/workspace-focus-selected)
file-id (get file :id)
vern (get file :vern)
page-id (get page :id)
objects (get page :objects)
background (get page :background clr/canvas)
@ -340,7 +341,7 @@
:opacity 0.6}}
(when (and (:can-edit permissions) (not read-only?))
[:& stvh/viewport-texts
{:key (dm/str "texts-" page-id)
{:key (dm/str "viewport-texts-" page-id "-" vern)
:page-id page-id
:objects objects
:modifiers modifiers
@ -366,7 +367,7 @@
:xmlnsXlink "http://www.w3.org/1999/xlink"
:xmlns:penpot "https://penpot.app/xmlns"
:preserveAspectRatio "xMidYMid meet"
:key (str "render" page-id)
:key (dm/str "viewport-svg-" page-id "-" vern)
:width (:width vport 0)
:height (:height vport 0)
:view-box (utils/format-viewbox vbox)
@ -400,7 +401,7 @@
[:& (mf/provider ctx/current-vbox) {:value vbox'}
[:& (mf/provider use/include-metadata-ctx) {:value (dbg/enabled? :show-export-metadata)}
;; Render root shape
[:& shapes/root-shape {:key page-id
[:& shapes/root-shape {:key (str page-id)
:objects base-objects
:active-frames @active-frames}]]]]
@ -408,7 +409,7 @@
{:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:preserveAspectRatio "xMidYMid meet"
:key (str "viewport" page-id)
:key (dm/str "viewport-controls-" page-id "-" vern)
:view-box (utils/format-viewbox vbox)
:ref on-viewport-ref
:class (dm/str @cursor (when drawing-tool " drawing") " " (stl/css :viewport-controls))
@ -719,7 +720,7 @@
(not= @hover-top-frame-id (:id frame)))
[:& grid-layout/editor
{:zoom zoom
:key (dm/str (:id frame))
:key (dm/str "viewport-frame-" (:id frame))
:objects base-objects
:modifiers modifiers
:shape frame
@ -733,8 +734,11 @@
:bottom-padding (when palete-size (+ palete-size 8))}]]]]]))
(mf/defc viewport*
[props]
(let [wasm-renderer-enabled? (features/use-feature "render-wasm/v1")]
(if ^boolean wasm-renderer-enabled?
[:> viewport.wasm/viewport* props]
[:> viewport-classic* props])))
[{:keys [file page] :as props}]
(let [vern (get file :vern)
page-id (get page :id)
render-wasm? (features/use-feature "render-wasm/v1")]
[:* {:key (dm/str "viewport-" page-id "-" vern)}
(if ^boolean render-wasm?
[:> viewport.wasm/viewport* props]
[:> viewport-classic* props])]))