mirror of
https://github.com/penpot/penpot.git
synced 2026-10-31 06:50:33 +00:00
♻️ Drop the ! suffix from the exporter namespaces
Mechanical rename, no behavior change: every function the exporter defines loses the `!`, at the definition and at every call site, in `src` and in the tests. Two names could not just drop it: the mutating `merge!` shares the namespace with a persistent `merge` of another contract, so it becomes `merge-into`; the test restore thunks take the role as their name (`fetch-restore`, `render-restore`). Two dead pieces go away with their bangs: the never-called `rmdir!` and an unused `mark!` binding in the cancel test. Untouched on purpose: the stdlib (`swap!`, `run!`, `try!`, …), the `set!` special form, `common` (`serialize-shape!`, `write-shape-*`), and the shared-key config the deploy still reads. AI-assisted-by: glm-5.3-flash
This commit is contained in:
parent
74f8d563b4
commit
b1cd524f9b
@ -5,6 +5,7 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.browser
|
||||
(:refer-clojure :exclude [eval])
|
||||
(:require
|
||||
["generic-pool" :as gp]
|
||||
["generic-pool/lib/errors.js" :as gpe]
|
||||
@ -38,8 +39,8 @@
|
||||
:name name
|
||||
:value token}]))
|
||||
|
||||
(defn nav!
|
||||
([page url] (nav! page url nil))
|
||||
(defn nav
|
||||
([page url] (nav page url nil))
|
||||
([page url {:keys [wait-until timeout] :or {wait-until "networkidle" timeout 20000}}]
|
||||
(.goto ^js page (str url) #js {:waitUntil wait-until :timeout timeout})))
|
||||
|
||||
@ -89,15 +90,15 @@
|
||||
([frame {:keys [full-page? omit-background? type quality path]
|
||||
:or {type "png" full-page? false omit-background? false quality 95}}]
|
||||
(let [options (-> (obj/new)
|
||||
(obj/set! "type" (name type))
|
||||
(obj/set! "omitBackground" omit-background?)
|
||||
(cond-> path (obj/set! "path" path))
|
||||
(cond-> (= "jpeg" type) (obj/set! "quality" quality))
|
||||
(cond-> full-page? (-> (obj/set! "fullPage" true)
|
||||
(obj/set! "clip" nil))))]
|
||||
(obj/set "type" (name type))
|
||||
(obj/set "omitBackground" omit-background?)
|
||||
(cond-> path (obj/set "path" path))
|
||||
(cond-> (= "jpeg" type) (obj/set "quality" quality))
|
||||
(cond-> full-page? (-> (obj/set "fullPage" true)
|
||||
(obj/set "clip" nil))))]
|
||||
(.screenshot ^js frame options))))
|
||||
|
||||
(defn emulate-media!
|
||||
(defn emulate-media
|
||||
[page {:keys [media]}]
|
||||
(.emulateMedia ^js page #js {:media media})
|
||||
page)
|
||||
@ -112,7 +113,7 @@
|
||||
:pageRanges page-ranges
|
||||
:printBackground true
|
||||
:preferCSSPageSize true})))
|
||||
(defn eval!
|
||||
(defn eval
|
||||
[frame f]
|
||||
(.evaluate ^js frame f))
|
||||
|
||||
@ -189,7 +190,7 @@
|
||||
:cause cause)
|
||||
(p/rejected cause)))
|
||||
|
||||
(defn exec!
|
||||
(defn exec
|
||||
[config handle]
|
||||
(letfn [(handle-browser [browser]
|
||||
(p/let [id (unchecked-get browser "__id")
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
|
||||
;; ---- PROGRESS AND CANCEL (the milestones of the contract)
|
||||
|
||||
(defn- start-beats!
|
||||
(defn- start-beats
|
||||
"The reporter of one run's beats.
|
||||
|
||||
The beat reports the stage and the counters and asks the backend to
|
||||
@ -85,29 +85,29 @@
|
||||
:cause cause)
|
||||
(p/resolved {:action :run})))
|
||||
(p/fmap check)))
|
||||
beats! (fn [stage counters & {:keys [force?]}]
|
||||
(let [now (js/Date.now)
|
||||
now? (or force?
|
||||
(>= (- now @last-beat) report-throttle-ms))]
|
||||
(if now?
|
||||
(do (reset! last-beat now)
|
||||
(reset! last {:stage stage :counters counters})
|
||||
(report stage counters))
|
||||
(p/resolved {:action :run :throttled? true}))))]
|
||||
{:beat! beats! :last last}))
|
||||
beat (fn [stage counters & {:keys [force?]}]
|
||||
(let [now (js/Date.now)
|
||||
now? (or force?
|
||||
(>= (- now @last-beat) report-throttle-ms))]
|
||||
(if now?
|
||||
(do (reset! last-beat now)
|
||||
(reset! last {:stage stage :counters counters})
|
||||
(report stage counters))
|
||||
(p/resolved {:action :run :throttled? true}))))]
|
||||
{:beat beat :last last}))
|
||||
|
||||
;; ---- THE WATCHDOG
|
||||
|
||||
(defn- with-watchdog!
|
||||
(defn- with-watchdog
|
||||
"The forced beats of one run: one every `watchdog-interval-ms`, and
|
||||
they repeat the last report the run made, so a cancellation that
|
||||
lands mid-render (no object landing, no unit of work ending) is seen
|
||||
within that interval. `stop!` clears it once the run settles."
|
||||
[beats! last]
|
||||
within that interval. `stop` clears it once the run settles."
|
||||
[beat last]
|
||||
(let [timer (js/setInterval
|
||||
#(beats! (:stage @last) (:counters @last) :force? true)
|
||||
#(beat (:stage @last) (:counters @last) :force? true)
|
||||
watchdog-interval-ms)]
|
||||
(fn stop! [] (js/clearInterval timer))))
|
||||
(fn stop [] (js/clearInterval timer))))
|
||||
|
||||
(defn- counter
|
||||
"One counter of the vocabulary: `kind` is `objects` or `pages`."
|
||||
@ -177,7 +177,7 @@
|
||||
|
||||
;; ---- THE CANCEL CHECK
|
||||
|
||||
(defn- check-cancelled!
|
||||
(defn- check-cancelled
|
||||
"The local arm of the cancel: the watchdog's terminate already died
|
||||
the in-flight thread; the units of work that follow check the flag
|
||||
and unwind without rendering anything else. The backend's row is
|
||||
@ -190,7 +190,7 @@
|
||||
|
||||
;; ---- THE RUNNERS
|
||||
|
||||
(defn- check-beat!
|
||||
(defn- check-beat
|
||||
"A forced beat after a unit of work: a `skip` there is a cancelled
|
||||
job, the same exception the legacy surfaces raised, so everything
|
||||
below unwinds."
|
||||
@ -201,34 +201,34 @@
|
||||
:hint "export job was cancelled"))
|
||||
answer)
|
||||
|
||||
(defn- run-single!
|
||||
(defn- run-single
|
||||
"One render, one object: the artifact IS the object, moved into the
|
||||
resource path the multipart settle will name."
|
||||
[beats! plan]
|
||||
[beat plan]
|
||||
(let [{:keys [job-id resource counter-kind]} plan
|
||||
export (-> plan :prepared first)
|
||||
object (atom nil)]
|
||||
(job.utils/track! job-id (:path resource))
|
||||
(p/let [beat-one (beats! :rendering (counter counter-kind 0 1) :force? true)
|
||||
_ (check-beat! beat-one)
|
||||
(job.utils/track job-id (:path resource))
|
||||
(p/let [beat-one (beat :rendering (counter counter-kind 0 1) :force? true)
|
||||
_ (check-beat beat-one)
|
||||
_ (rd/render (assoc export
|
||||
:job-id job-id
|
||||
:skip-children (:skip-children plan))
|
||||
(fn [obj]
|
||||
(reset! object obj)
|
||||
(job.utils/track! job-id (:path obj))))
|
||||
_ (sh/move! (:path @object) (:path resource))
|
||||
beat-end (beats! :packaging (counter counter-kind 1 1) :force? true)
|
||||
_ (check-beat! beat-end)]
|
||||
(job.utils/track job-id (:path obj))))
|
||||
_ (sh/move (:path @object) (:path resource))
|
||||
beat-end (beat :packaging (counter counter-kind 1 1) :force? true)
|
||||
_ (check-beat beat-end)]
|
||||
resource)))
|
||||
|
||||
(defn- run-multiple!
|
||||
(defn- run-multiple
|
||||
"The multi-object render: every prepared export renders with the same
|
||||
on-object callback, appending each object to the zip as it lands, and
|
||||
the progress counts what has landed so far. Any zipping error
|
||||
surfaces after the renders, so the failures of the writer do not
|
||||
compete with the ones of the render."
|
||||
[beats! cancelled plan]
|
||||
[beat cancelled plan]
|
||||
(let [{:keys [job-id resource prepared total counter-kind]} plan
|
||||
failure (volatile! nil)
|
||||
rendered (volatile! 0)
|
||||
@ -236,17 +236,17 @@
|
||||
:on-error (fn [cause] (vreset! failure cause))
|
||||
:on-progress (fn [_] nil))
|
||||
append (fn [{:keys [filename path]}]
|
||||
(check-cancelled! cancelled)
|
||||
(job.utils/track! job-id path)
|
||||
(check-cancelled cancelled)
|
||||
(job.utils/track job-id path)
|
||||
(vswap! rendered inc)
|
||||
(beats! :rendering
|
||||
(counter counter-kind @rendered total))
|
||||
(beat :rendering
|
||||
(counter counter-kind @rendered total))
|
||||
(rsc/add-to-zip zip path
|
||||
(str/replace filename
|
||||
plan/sanitize-file-regex
|
||||
"_")))]
|
||||
(p/let [beat-one (beats! :rendering (counter counter-kind 0 total) :force? true)
|
||||
_ (check-beat! beat-one)
|
||||
(p/let [beat-one (beat :rendering (counter counter-kind 0 total) :force? true)
|
||||
_ (check-beat beat-one)
|
||||
_ (rd/with-scope prepared
|
||||
(fn [scoped-render]
|
||||
(p/all (map (fn [export]
|
||||
@ -258,37 +258,37 @@
|
||||
(p/rejected cause)
|
||||
(rsc/close-zip zip))
|
||||
_ (do error)
|
||||
beat-end (beats! :packaging (counter counter-kind total total) :force? true)
|
||||
_ (check-beat! beat-end)]
|
||||
beat-end (beat :packaging (counter counter-kind total total) :force? true)
|
||||
_ (check-beat beat-end)]
|
||||
resource)))
|
||||
|
||||
(defn- join-pdf!
|
||||
(defn- join-pdf
|
||||
"The pages render one file per page; `pdfunite` stitches one pdf out
|
||||
of them all, the way the legacy export did."
|
||||
[job-id file-id paths]
|
||||
(let [path (job.utils/track! job-id
|
||||
(sh/tempfile :prefix (str/concat "penpot.pdfunite." file-id ".")
|
||||
:suffix ".pdf"))]
|
||||
(p/let [_ (sh/run-cmd! "pdfunite" (into [] (concat (vec paths) [path])))]
|
||||
(let [path (job.utils/track job-id
|
||||
(sh/tempfile :prefix (str/concat "penpot.pdfunite." file-id ".")
|
||||
:suffix ".pdf"))]
|
||||
(p/let [_ (sh/run-cmd "pdfunite" (into [] (concat (vec paths) [path])))]
|
||||
path)))
|
||||
|
||||
(defn- run-frames!
|
||||
(defn- run-frames
|
||||
"The frames render: a file per page, joined into the pdf of the file
|
||||
once every page has landed."
|
||||
[beats! cancelled plan]
|
||||
[beat cancelled plan]
|
||||
(let [{:keys [job-id resource prepared total]} plan
|
||||
file-id (-> prepared first :file-id)
|
||||
paths (volatile! [])
|
||||
rendered (volatile! 0)
|
||||
on-object (fn [{:keys [path]}]
|
||||
(check-cancelled! cancelled)
|
||||
(job.utils/track! job-id path)
|
||||
(check-cancelled cancelled)
|
||||
(job.utils/track job-id path)
|
||||
(vswap! paths conj path)
|
||||
(vswap! rendered inc)
|
||||
(beats! :rendering
|
||||
(counter :pages @rendered total)))]
|
||||
(p/let [beat-one (beats! :rendering (counter :pages 0 total) :force? true)
|
||||
_ (check-beat! beat-one)
|
||||
(beat :rendering
|
||||
(counter :pages @rendered total)))]
|
||||
(p/let [beat-one (beat :rendering (counter :pages 0 total) :force? true)
|
||||
_ (check-beat beat-one)
|
||||
_ (rd/with-scope prepared
|
||||
(fn [scoped-render]
|
||||
(p/all (map (fn [export]
|
||||
@ -297,18 +297,18 @@
|
||||
:is-wasm (:is-wasm plan))
|
||||
on-object))
|
||||
prepared))))
|
||||
joined (join-pdf! job-id file-id @paths)
|
||||
_ (sh/move! joined (:path resource))
|
||||
beat-end (beats! :packaging (counter :pages total total) :force? true)
|
||||
_ (check-beat! beat-end)]
|
||||
joined (join-pdf job-id file-id @paths)
|
||||
_ (sh/move joined (:path resource))
|
||||
beat-end (beat :packaging (counter :pages total total) :force? true)
|
||||
_ (check-beat beat-end)]
|
||||
resource)))
|
||||
|
||||
(defn- run-prepared!
|
||||
[beats! cancelled plan]
|
||||
(defn- run-prepared
|
||||
[beat cancelled plan]
|
||||
(cond
|
||||
(:single? plan) (run-single! beats! plan)
|
||||
(:frames? plan) (run-frames! beats! cancelled plan)
|
||||
:else (run-multiple! beats! cancelled plan)))
|
||||
(:single? plan) (run-single beat plan)
|
||||
(:frames? plan) (run-frames beat cancelled plan)
|
||||
:else (run-multiple beat cancelled plan)))
|
||||
|
||||
;; ---- THE SETTLE
|
||||
|
||||
@ -329,21 +329,21 @@
|
||||
[cause]
|
||||
(= :job-cancelled (:code (ex-data cause))))
|
||||
|
||||
(defn- settle-failure!
|
||||
(defn- settle-failure
|
||||
"Settles the job as failed, after releasing the temp files the run
|
||||
owns. The settle never rejects: if the failure cannot reach the
|
||||
backend, the row keeps running until the lease GC turns it `aborted`,
|
||||
which is the honest state for a worker that cannot talk."
|
||||
[job-id session cause]
|
||||
(p/do
|
||||
(job.utils/release! job-id)
|
||||
(job.utils/release job-id)
|
||||
(->> (api/fail-job job-id (failure-report cause)
|
||||
:session-id (some-> @session :session-id))
|
||||
(p/merr (fn [settle-cause]
|
||||
(l/warn :hint "unable to settle the failed job"
|
||||
:job-id job-id :cause settle-cause))))))
|
||||
|
||||
(defn run-export!
|
||||
(defn run-export
|
||||
[{:keys [job-id]} params]
|
||||
(let [session (atom nil)
|
||||
cancelled (atom false)
|
||||
@ -351,46 +351,46 @@
|
||||
;; stopper travels in this atom: a run that ends badly stops
|
||||
;; beating too, or its interval would knock on a dead job
|
||||
;; forever, one HTTP call a second that only ever answers skip
|
||||
stop-watchdog! (atom nil)
|
||||
_ (jobs/register! job-id)
|
||||
stop-cmd! (fn []
|
||||
(reset! cancelled true)
|
||||
;; the hard-cancel of the render the run holds: the
|
||||
;; `renderer.wasm/with-scope` of the in-flight render
|
||||
;; registered a terminate callback for this job
|
||||
(jobs/mark-cancelled job-id))]
|
||||
stop-watchdog (atom nil)
|
||||
_ (jobs/register job-id)
|
||||
stop-cmd (fn []
|
||||
(reset! cancelled true)
|
||||
;; the hard-cancel of the render the run holds: the
|
||||
;; `renderer.wasm/with-scope` of the in-flight render
|
||||
;; registered a terminate callback for this job
|
||||
(jobs/mark-cancelled job-id))]
|
||||
(p/catch
|
||||
(p/let [beats (start-beats! job-id stop-cmd!)
|
||||
beats! (:beat! beats)
|
||||
_ (reset! stop-watchdog! (with-watchdog! beats! (:last beats)))
|
||||
(p/let [beats (start-beats job-id stop-cmd)
|
||||
beat (:beat beats)
|
||||
_ (reset! stop-watchdog (with-watchdog beat (:last beats)))
|
||||
;; the first breath right after the claim, before the
|
||||
;; session roundtrip: the widget moves from queued to
|
||||
;; exporting on it, and a cancel that landed meanwhile
|
||||
;; raises here instead of after wasted work
|
||||
_ (check-beat! (beats! :preparing {} :force? true))
|
||||
_ (check-beat (beat :preparing {} :force? true))
|
||||
session' (api/create-job-session job-id)
|
||||
_ (reset! session session')
|
||||
token (:session-token session')
|
||||
_ (l/info :hint "render session minted"
|
||||
:job-id (str job-id))
|
||||
plan (make-plan job-id token params)
|
||||
resource (run-prepared! beats! cancelled plan)
|
||||
_ (@stop-watchdog!)
|
||||
resource (run-prepared beat cancelled plan)
|
||||
_ (@stop-watchdog)
|
||||
artifact {:path (str (:path resource))
|
||||
:filename (:filename resource)
|
||||
:mtype (:mtype resource)}
|
||||
_ (api/complete-job-with-artifact
|
||||
{:job-id job-id :session-id (:session-id session')}
|
||||
artifact)
|
||||
_ (job.utils/release! job-id)
|
||||
_ (job.utils/release job-id)
|
||||
_ (l/info :hint "export job settled" :job-id (str job-id)
|
||||
:outcome "completed")]
|
||||
nil)
|
||||
(fn [cause]
|
||||
(when-let [stop @stop-watchdog!] (stop))
|
||||
(when-let [stop @stop-watchdog] (stop))
|
||||
(if (cancelled? cause)
|
||||
(l/info :hint "export job cancelled" :job-id (str job-id))
|
||||
(l/error :hint "export job failed" :job-id (str job-id) :cause cause))
|
||||
(settle-failure! job-id session cause)))))
|
||||
(settle-failure job-id session cause)))))
|
||||
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
[]
|
||||
(ccfg/queue-key))
|
||||
|
||||
(defn- connect!
|
||||
(defn- connect
|
||||
"One Redis connection of one poller. ioredis reconnects on its own
|
||||
strategy, so a fallen Redis comes back with this process doing
|
||||
nothing but wait (and the failed pop comes right after)."
|
||||
@ -49,7 +49,7 @@
|
||||
|
||||
;; ---- THE QUEUE
|
||||
|
||||
(defn read-payload!
|
||||
(defn read-payload
|
||||
"The JSON payload of the dispatcher: `job-id` and the `scheduled-at`
|
||||
of the row when it was pushed. Anything else is a corrupt payload:
|
||||
none, and the caller drops it."
|
||||
@ -62,7 +62,7 @@
|
||||
:payload (str payload) :cause cause)
|
||||
nil)))
|
||||
|
||||
(defn process!
|
||||
(defn process
|
||||
"One payload of the queue: claim the job, and when the backend gives
|
||||
it to this poller, run the export to its settle. `skip` means
|
||||
something else won it (or it is gone): the payload is dropped, with
|
||||
@ -74,30 +74,30 @@
|
||||
:name (:name answer) :status (:status answer))
|
||||
(p/do
|
||||
(l/info :hint "running job" :job-id (str job-id) :name (:name answer))
|
||||
(exports/run-export! {:job-id job-id} (:params answer))
|
||||
(exports/run-export {:job-id job-id} (:params answer))
|
||||
(l/info :hint "job settled" :job-id (str job-id))))))
|
||||
|
||||
;; ---- THE POLLER
|
||||
|
||||
(defn poll-once!
|
||||
(defn poll-once
|
||||
"One turn: pop the queue, claim what came, and wait for the settle.
|
||||
An empty pop and a dropped payload both resolve; a claimed job is
|
||||
awaited the whole run."
|
||||
[conn key on-payload]
|
||||
(p/let [answer (.blpop ^js conn key poll-timeout-s)
|
||||
payload (second answer)
|
||||
claim (when (some? payload) (read-payload! payload))
|
||||
claim (when (some? payload) (read-payload payload))
|
||||
settled (when (some? claim) (on-payload claim))]
|
||||
settled))
|
||||
|
||||
(defn- poll-loop!
|
||||
(defn- poll-loop
|
||||
"The wait of one poller, turn after turn, for the life of the
|
||||
process. A failed turn (Redis down, a bug) is logged, never allowed
|
||||
to end the loop; a delay keeps the retry from spinning while the
|
||||
connection rebuilds."
|
||||
[conn key on-payload]
|
||||
(p/loop []
|
||||
(->> (poll-once! conn key on-payload)
|
||||
(->> (poll-once conn key on-payload)
|
||||
(p/merr (fn [cause]
|
||||
(l/error :hint "poller turn failed" :key key :cause cause)
|
||||
(p/delay reconnect-delay-ms)))
|
||||
@ -113,7 +113,7 @@
|
||||
|
||||
;; ---- THE START
|
||||
|
||||
(defn start!
|
||||
(defn start
|
||||
"Launches `K` pollers, one connection each. The worker cleans the
|
||||
temp files a previous process left behind at the same moment: a
|
||||
crashed worker owns nothing of its crash."
|
||||
@ -127,12 +127,12 @@
|
||||
(l/info :hint "worker started" :pollers k :queue key
|
||||
:redis-host (redis-host) :backend (str (cf/get-internal-uri)))
|
||||
(doseq [i (range k)]
|
||||
(let [conn (connect!)]
|
||||
(let [conn (connect)]
|
||||
(swap! pollers assoc i conn)
|
||||
(l/info :hint "poller started" :id i :key key)
|
||||
(poll-loop! conn key process!)))))
|
||||
(poll-loop conn key process)))))
|
||||
|
||||
(defn stop!
|
||||
(defn stop
|
||||
"Ends the pollers, closing their connections. The job that got orphan
|
||||
in a settle goes one row further to the next poller; no state hides
|
||||
in this process."
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
(bwr/init)
|
||||
(job.utils/init)
|
||||
(wasm.pool/init)
|
||||
(worker/start!)))))
|
||||
(worker/start)))))
|
||||
|
||||
(def main start)
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
;; the pollers own their connections and may sit in a blocking
|
||||
;; pop for the whole poll timeout; draining the pools first lets
|
||||
;; the exports in flight unwind against their own resources
|
||||
(shutdown-step "consumer-worker" worker/stop!)
|
||||
(shutdown-step "consumer-worker" worker/stop)
|
||||
(done)))))
|
||||
|
||||
(.on proc/default "uncaughtException"
|
||||
|
||||
@ -64,13 +64,13 @@
|
||||
(swap! registry update (str job-id)
|
||||
(fn [rt] (some-> rt (update :cancel-fns (fnil conj []) f)))))
|
||||
|
||||
(defn release!
|
||||
(defn release
|
||||
"Drops the local runtime entry once the job settled. The row stays in
|
||||
the backend until it is read once more or expires."
|
||||
[job-id]
|
||||
(swap! registry dissoc (str job-id)))
|
||||
|
||||
(defn register!
|
||||
(defn register
|
||||
"The runtime record of a job this process runs. The local cancel arm
|
||||
reads it (`mark-cancelled`), so the in-flight render of a claimed job
|
||||
is terminable without the jobs substrate: the row is the backend's
|
||||
@ -80,7 +80,7 @@
|
||||
{:job {:id job-id :state "running" :total total}
|
||||
:cancelled? false}))
|
||||
|
||||
(defn- check-cancel-callback!
|
||||
(defn- check-cancel-callback
|
||||
[job-id f]
|
||||
(try
|
||||
(f)
|
||||
@ -110,5 +110,5 @@
|
||||
(when-let [signal (:cancel-signal (get @registry k))]
|
||||
(js/Atomics.store signal 0 1))
|
||||
(doseq [f callbacks]
|
||||
(check-cancel-callback! job-id f)))
|
||||
(check-cancel-callback job-id f)))
|
||||
true))))
|
||||
|
||||
@ -24,21 +24,21 @@
|
||||
|
||||
(defonce ^:private tracked (atom {}))
|
||||
|
||||
(defn track!
|
||||
(defn track
|
||||
"Registers `path` as owned by `job-id`, so it is removed when the job settles."
|
||||
[job-id path]
|
||||
(when (and job-id path)
|
||||
(swap! tracked update (str job-id) (fnil conj #{}) path))
|
||||
path)
|
||||
|
||||
(defn- remove-path!
|
||||
(defn- remove-path
|
||||
[path]
|
||||
(->> (p/do (fsp/rm path #js {:recursive true :force true}))
|
||||
(p/merr (fn [cause]
|
||||
(l/warn :hint "unable to remove job temp file" :path path :cause cause)
|
||||
(p/resolved nil)))))
|
||||
|
||||
(defn release!
|
||||
(defn release
|
||||
"Removes every file the job owns. Called once the job reached a terminal
|
||||
state and its result has already been uploaded, so nothing else reads them."
|
||||
[job-id]
|
||||
@ -46,14 +46,14 @@
|
||||
paths (get @tracked k)]
|
||||
(swap! tracked dissoc k)
|
||||
(if (seq paths)
|
||||
(->> (map remove-path! paths)
|
||||
(->> (map remove-path paths)
|
||||
(p/all)
|
||||
(p/fmap (fn [_]
|
||||
(l/dbg :hint "released job temp files" :job-id k :count (count paths))
|
||||
nil)))
|
||||
(p/resolved nil))))
|
||||
|
||||
(defn- clean!
|
||||
(defn- clean
|
||||
"Removes managed temp files older than the job TTL. They can only be leftovers
|
||||
of a previous process: every live one belongs to a job of this process."
|
||||
[]
|
||||
@ -67,7 +67,7 @@
|
||||
(->> (p/do (fsp/stat fpath))
|
||||
(p/mcat (fn [^js stat]
|
||||
(if (> (- now (inst-ms (.-mtime stat))) max-age)
|
||||
(->> (remove-path! fpath)
|
||||
(->> (remove-path fpath)
|
||||
(p/fmap (constantly 1)))
|
||||
(p/resolved 0))))
|
||||
(p/merr (fn [_] (p/resolved 0)))))))
|
||||
@ -83,4 +83,4 @@
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(clean!))
|
||||
(clean))
|
||||
|
||||
@ -38,17 +38,17 @@
|
||||
:webp (p/let [png-path (sh/tempfile :prefix "penpot.tmp.bitmap." :suffix ".png")]
|
||||
;; playwright only supports jpg and png, we need to convert it afterwards
|
||||
(bw/screenshot node {:omit-background? true :type :png :path png-path})
|
||||
(sh/run-cmd! "convert" png-path "-quality" "100" (str "WEBP:" path))))
|
||||
(sh/run-cmd "convert" png-path "-quality" "100" (str "WEBP:" path))))
|
||||
(on-object (assoc object :path path))))
|
||||
|
||||
(render [uri page]
|
||||
(l/info :uri uri)
|
||||
(p/do
|
||||
;; navigate to the page and perform basic setup
|
||||
(bw/nav! page (str uri))
|
||||
(bw/nav page (str uri))
|
||||
(bw/sleep page 1000) ; the good old fix with sleep
|
||||
(bw/wait-for-fonts page)
|
||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||
(bw/eval page (js* "() => document.body.style.background = 'transparent'"))
|
||||
|
||||
;; take the screnshot of requested objects, one by one
|
||||
(p/run (partial render-object page) objects)
|
||||
@ -64,4 +64,4 @@
|
||||
(u/ensure-path-slash)
|
||||
(u/join "render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec! (prepare-options uri) (partial render uri)))))
|
||||
(bw/exec (prepare-options uri) (partial render uri)))))
|
||||
|
||||
@ -38,32 +38,32 @@
|
||||
(u/join "render.html")
|
||||
(assoc :query (u/map->query-string params)))))
|
||||
|
||||
(sync-page-size! [dom]
|
||||
(bw/eval! dom
|
||||
(fn [elem]
|
||||
;; IMPORTANT: No CLJS runtime allowed. Use only JS
|
||||
;; primitives. This runs in a context without access to
|
||||
;; cljs.core. Avoid any functions that transpile to
|
||||
;; cljs.core/* calls, as they will break in the browser
|
||||
;; runtime.
|
||||
(sync-page-size [dom]
|
||||
(bw/eval dom
|
||||
(fn [elem]
|
||||
;; IMPORTANT: No CLJS runtime allowed. Use only JS
|
||||
;; primitives. This runs in a context without access to
|
||||
;; cljs.core. Avoid any functions that transpile to
|
||||
;; cljs.core/* calls, as they will break in the browser
|
||||
;; runtime.
|
||||
|
||||
(let [width (.getAttribute ^js elem "width")
|
||||
height (.getAttribute ^js elem "height")
|
||||
style-node (let [node (.createElement js/document "style")]
|
||||
(.appendChild (.-head js/document) node)
|
||||
node)]
|
||||
(set! (.-textContent style-node)
|
||||
(dm/str "@page { size: " width "px " height "px; margin: 0; }\n"
|
||||
"html, body, #app { margin: 0; padding: 0; width: " width "px; height: " height "px; overflow: visible; }"))))))
|
||||
(let [width (.getAttribute ^js elem "width")
|
||||
height (.getAttribute ^js elem "height")
|
||||
style-node (let [node (.createElement js/document "style")]
|
||||
(.appendChild (.-head js/document) node)
|
||||
node)]
|
||||
(set! (.-textContent style-node)
|
||||
(dm/str "@page { size: " width "px " height "px; margin: 0; }\n"
|
||||
"html, body, #app { margin: 0; padding: 0; width: " width "px; height: " height "px; overflow: visible; }"))))))
|
||||
|
||||
(render-object [page base-uri {:keys [id] :as object}]
|
||||
(p/let [uri (prepare-uri base-uri id)
|
||||
path (sh/tempfile :prefix "penpot.tmp.pdf." :suffix (mime/get-extension type))]
|
||||
(l/info :uri uri)
|
||||
(bw/nav! page uri)
|
||||
(bw/nav page uri)
|
||||
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
|
||||
(bw/wait-for dom)
|
||||
(sync-page-size! dom)
|
||||
(sync-page-size dom)
|
||||
(bw/screenshot dom {:full-page? true})
|
||||
(bw/sleep page 2000) ; the good old fix with sleep
|
||||
(bw/wait-for-fonts page)
|
||||
@ -79,5 +79,5 @@
|
||||
|
||||
(let [base-uri (-> (cf/get-internal-uri)
|
||||
(u/ensure-path-slash))]
|
||||
(bw/exec! (prepare-options base-uri)
|
||||
(partial render base-uri)))))
|
||||
(bw/exec (prepare-options base-uri)
|
||||
(partial render base-uri)))))
|
||||
|
||||
@ -128,13 +128,13 @@
|
||||
(letfn [(convert-to-ppm [pngpath]
|
||||
(let [ppmpath (str/concat pngpath "origin.ppm")]
|
||||
(l/trace :fn :convert-to-ppm :path ppmpath)
|
||||
(-> (sh/run-cmd! "convert" pngpath ppmpath)
|
||||
(-> (sh/run-cmd "convert" pngpath ppmpath)
|
||||
(p/then (constantly ppmpath)))))
|
||||
|
||||
(trace-color-mask [pbmpath]
|
||||
(l/trace :fn :trace-color-mask :pbmpath pbmpath)
|
||||
(let [svgpath (str/concat pbmpath ".svg")]
|
||||
(-> (sh/run-cmd! "potrace" "--flat" "-b" "svg" pbmpath "-o" svgpath)
|
||||
(-> (sh/run-cmd "potrace" "--flat" "-b" "svg" pbmpath "-o" svgpath)
|
||||
(p/then (constantly svgpath)))))
|
||||
|
||||
(generate-color-layer [ppmpath color]
|
||||
@ -144,9 +144,9 @@
|
||||
:hint (str "invalid hex color: " color)))
|
||||
(l/trace :fn :generate-color-layer :ppmpath ppmpath :color color)
|
||||
(let [pbmpath (str/concat ppmpath ".mask-" (subs color 1) ".pbm")]
|
||||
(-> (sh/run-cmd! "ppmcolormask" color ppmpath)
|
||||
(-> (sh/run-cmd "ppmcolormask" color ppmpath)
|
||||
(p/then (fn [stdout]
|
||||
(-> (sh/write-file! pbmpath stdout)
|
||||
(-> (sh/write-file pbmpath stdout)
|
||||
(p/then (constantly pbmpath)))))
|
||||
(p/then trace-color-mask)
|
||||
(p/then sh/read-file)
|
||||
@ -236,7 +236,7 @@
|
||||
(l/trace :fn :trace-node)
|
||||
(p/let [pngpath (sh/tempfile :prefix "penpot.tmp.render.svg.parse."
|
||||
:suffix ".origin.png")
|
||||
_ (sh/write-file! pngpath data)
|
||||
_ (sh/write-file pngpath data)
|
||||
ppmpath (convert-to-ppm pngpath)
|
||||
svgdata (convert-to-svg ppmpath node)]
|
||||
(-> node
|
||||
@ -258,7 +258,7 @@
|
||||
(extract-single-node [[shot node]]
|
||||
(l/trace :fn :extract-single-node)
|
||||
|
||||
(p/let [attrs (bw/eval! node extract-element-attrs)]
|
||||
(p/let [attrs (bw/eval node extract-element-attrs)]
|
||||
{:id (unchecked-get attrs "id")
|
||||
:x (unchecked-get attrs "x")
|
||||
:y (unchecked-get attrs "y")
|
||||
@ -269,7 +269,7 @@
|
||||
:data shot}))
|
||||
|
||||
(resolve-text-node [page node]
|
||||
(p/let [attrs (bw/eval! node extract-element-attrs)
|
||||
(p/let [attrs (bw/eval node extract-element-attrs)
|
||||
id (unchecked-get attrs "id")
|
||||
text-node (bw/select page (str "#screenshot-text-" id " foreignObject"))
|
||||
shot (bw/screenshot text-node {:omit-background? true :type "png"})]
|
||||
@ -290,7 +290,7 @@
|
||||
(extract-svg [page {:keys [id] :as object}]
|
||||
(let [node (bw/select page (str/concat "#screenshot-" id))]
|
||||
(bw/wait-for node)
|
||||
(bw/eval! node (fn [elem] (.-outerHTML ^js elem)))))
|
||||
(bw/eval node (fn [elem] (.-outerHTML ^js elem)))))
|
||||
|
||||
(prepare-options [uri]
|
||||
#js {:screen #js {:width bw/default-viewport-width
|
||||
@ -327,7 +327,7 @@
|
||||
;; (cljs.pprint/pprint (xml->clj result))
|
||||
;; (println "-------")
|
||||
|
||||
(sh/write-file! path result)
|
||||
(sh/write-file path result)
|
||||
(on-object (assoc object :path path))
|
||||
path)))
|
||||
|
||||
@ -335,7 +335,7 @@
|
||||
(l/info :uri uri)
|
||||
(p/do
|
||||
;; navigate to the page and perform basic setup
|
||||
(bw/nav! page (str uri))
|
||||
(bw/nav page (str uri))
|
||||
(bw/sleep page 1000) ; the good old fix with sleep
|
||||
(bw/wait-for-fonts page)
|
||||
|
||||
@ -352,5 +352,5 @@
|
||||
(u/ensure-path-slash)
|
||||
(u/join "render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec! (prepare-options uri)
|
||||
(partial render uri)))))
|
||||
(bw/exec (prepare-options uri)
|
||||
(partial render uri)))))
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
;; Between objects the worker sees the flag; inside a render only
|
||||
;; terminating the thread stops it. Cleared on the way out so a later
|
||||
;; cancel cannot terminate a worker that is by then somebody else's.
|
||||
(jobs/on-cancel job-id (fn [] (pool/terminate! @live))))
|
||||
(jobs/on-cancel job-id (fn [] (pool/terminate @live))))
|
||||
(->> (p/do (f (fn [params on-object]
|
||||
(chain #(pool/render-on worker params on-object opts)))))
|
||||
(p/fnly (fn [_ _] (vreset! live nil))))))))
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.util.object
|
||||
"A collection of helpers for work with javascript objects."
|
||||
(:refer-clojure :exclude [set! get get-in merge clone contains?])
|
||||
(:refer-clojure :exclude [set get get-in merge update clone contains?])
|
||||
(:require
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@ -46,24 +46,24 @@
|
||||
[a]
|
||||
(js/Object.assign #js {} a))
|
||||
|
||||
(defn merge!
|
||||
(defn merge-into
|
||||
([a b]
|
||||
(js/Object.assign a b))
|
||||
([a b & more]
|
||||
(reduce merge! (merge! a b) more)))
|
||||
(reduce merge-into (merge-into a b) more)))
|
||||
|
||||
(defn merge
|
||||
([a b]
|
||||
(js/Object.assign #js {} a b))
|
||||
([a b & more]
|
||||
(reduce merge! (merge a b) more)))
|
||||
(reduce merge-into (merge a b) more)))
|
||||
|
||||
(defn set!
|
||||
[obj key value]
|
||||
(unchecked-set obj key value)
|
||||
obj)
|
||||
|
||||
(defn update!
|
||||
(defn update
|
||||
[obj key f & args]
|
||||
(let [found (get obj key ::not-found)]
|
||||
(if-not (identical? ::not-found found)
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
:code :unable-to-locate-temporal-file
|
||||
:hint "unable to find a tempfile candidate"))))
|
||||
|
||||
(defn move!
|
||||
(defn move
|
||||
[origin-path dest-path]
|
||||
(.rename fs/promises origin-path dest-path))
|
||||
|
||||
@ -81,11 +81,7 @@
|
||||
(p/merr (fn [_cause]
|
||||
(p/resolved nil)))))
|
||||
|
||||
(defn rmdir!
|
||||
[path]
|
||||
(.rm fs/promises path #js {:recursive true}))
|
||||
|
||||
(defn write-file!
|
||||
(defn write-file
|
||||
[fpath content]
|
||||
(.writeFile fs/promises fpath content))
|
||||
|
||||
@ -93,7 +89,7 @@
|
||||
[fpath]
|
||||
(.readFile fs/promises fpath))
|
||||
|
||||
(defn run-cmd!
|
||||
(defn run-cmd
|
||||
[cmd & args]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
|
||||
@ -56,11 +56,11 @@
|
||||
|
||||
;; --- MODULE LIFECYCLE
|
||||
|
||||
(defn init!
|
||||
(defn init
|
||||
"Loads the render-wasm artifact under Node and boots it headless. Sets the
|
||||
shared `wasm/internal-module` so the portable serialization leaves work.
|
||||
Idempotent-ish: callers should hold the returned module."
|
||||
([] (init! default-viewport-width default-viewport-height))
|
||||
([] (init default-viewport-width default-viewport-height))
|
||||
([width height]
|
||||
(let [dir artifact-dir
|
||||
js-path (path/resolve dir "render-wasm.js")
|
||||
@ -130,7 +130,7 @@
|
||||
(d/distinct-xf font-key))
|
||||
shape-ids))
|
||||
|
||||
(defn store-font!
|
||||
(defn store-font
|
||||
"Uploads one font's TTF bytes into the WASM font store, keyed by the family
|
||||
(uuid quartet + weight + style). `font-bytes` is a Uint8Array/Buffer.
|
||||
|
||||
@ -147,12 +147,12 @@
|
||||
(aget id 0) (aget id 1) (aget id 2) (aget id 3)
|
||||
weight style (boolean emoji?) (boolean fallback?))))
|
||||
|
||||
(defn store-font-url!
|
||||
(defn store-font-url
|
||||
"Registers the public URL a font family was loaded from. The SVG export emits
|
||||
one `@font-face` per family from these, and skips families without one, so
|
||||
this must run for every family `store-font!` uploads.
|
||||
this must run for every family `store-font` uploads.
|
||||
|
||||
Does NOT call `mem/free`, for the same reason as `store-font!`."
|
||||
Does NOT call `mem/free`, for the same reason as `store-font`."
|
||||
[{:keys [id weight style]} url]
|
||||
(let [bytes (js/Buffer.from url "utf-8")
|
||||
ptr (mem/alloc (.-byteLength bytes))]
|
||||
@ -161,16 +161,16 @@
|
||||
(aget id 0) (aget id 1) (aget id 2) (aget id 3)
|
||||
weight style)))
|
||||
|
||||
(defn clear-fonts!
|
||||
(defn clear-fonts
|
||||
"Resets the WASM font store. Must be called once per render request because
|
||||
the shared module would otherwise accumulate fonts across requests."
|
||||
[]
|
||||
(h/call wasm/internal-module "_clear_fonts"))
|
||||
|
||||
(defn update-text-layout!
|
||||
(defn update-text-layout
|
||||
"Recomputes a text shape's layout with the currently provisioned fonts. Text is
|
||||
laid out at serialize time using the fallback font (real fonts aren't uploaded
|
||||
yet), so this must run again after `provision-fonts!` or glyph metrics/line
|
||||
yet), so this must run again after `provision-fonts` or glyph metrics/line
|
||||
breaks are wrong.
|
||||
|
||||
Forced, because provisioning a font changes nothing `update_layout` keys on:
|
||||
@ -191,13 +191,13 @@
|
||||
(aget buf 0) (aget buf 1) (aget buf 2) (aget buf 3)
|
||||
false)))))
|
||||
|
||||
(defn store-image-url!
|
||||
(defn store-image-url
|
||||
"Registers the public URL an image was loaded from. The SVG export emits
|
||||
linked `<image href>` from these, and falls back to Skia base64 when missing,
|
||||
so this should run for every media id the scene references (including
|
||||
already-cached images).
|
||||
|
||||
Does NOT call `mem/free`, for the same reason as `store-font-url!`."
|
||||
Does NOT call `mem/free`, for the same reason as `store-font-url`."
|
||||
[image-id url]
|
||||
(when (and (some? url) (not (str/blank? url)))
|
||||
(let [bytes (js/Buffer.from url "utf-8")
|
||||
@ -207,7 +207,7 @@
|
||||
(h/call wasm/internal-module "_store_image_url"
|
||||
(aget quart 0) (aget quart 1) (aget quart 2) (aget quart 3)))))
|
||||
|
||||
(defn store-image!
|
||||
(defn store-image
|
||||
"Uploads one image's *encoded* bytes (PNG/JPEG — Skia decodes, no WebGL) into
|
||||
the WASM image store via `_store_image`. Buffer layout matches the Rust reader:
|
||||
[shape uuid 16][image uuid 16][is_thumbnail u32][encoded bytes]. Images are
|
||||
@ -239,13 +239,13 @@
|
||||
(.set heap img-u8 (+ ptr 36))
|
||||
(h/call module "_store_image")))
|
||||
|
||||
(defn evict-images!
|
||||
(defn evict-images
|
||||
"Evicts least-recently-used images until the store retains at most `max-bytes`
|
||||
bytes. Returns the number evicted."
|
||||
[max-bytes]
|
||||
(h/call wasm/internal-module "_evict_images_to_budget" max-bytes))
|
||||
|
||||
(defn provision-fonts!
|
||||
(defn provision-fonts
|
||||
"Resolves and uploads every font needed by `shape-ids`, each family fetched
|
||||
once. `resolve-font` is an injected fn of the family map -> promise of TTF
|
||||
bytes (or nil to skip); optional `font-url` is a fn of the family map -> the
|
||||
@ -257,9 +257,9 @@
|
||||
(->> (resolve-font family)
|
||||
(p/fmap (fn [bytes]
|
||||
(when bytes
|
||||
(store-font! family bytes)
|
||||
(store-font family bytes)
|
||||
(when-let [url (when font-url (font-url family))]
|
||||
(store-font-url! family url))))))))
|
||||
(store-font-url family url))))))))
|
||||
(p/all)))
|
||||
|
||||
;; --- RENDER
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
:hint "export job was cancelled"))
|
||||
(run-on-worker worker params cancel-buffer on-object)))
|
||||
|
||||
(defn terminate!
|
||||
(defn terminate
|
||||
[^js worker]
|
||||
(when worker
|
||||
(unchecked-set worker "__alive" false)
|
||||
|
||||
@ -52,10 +52,10 @@
|
||||
|
||||
(defonce ^:private module* (atom nil))
|
||||
|
||||
(defn- ensure-module!
|
||||
(defn- ensure-module
|
||||
[]
|
||||
(or @module*
|
||||
(reset! module* (wasm/init!))))
|
||||
(reset! module* (wasm/init))))
|
||||
|
||||
;; --- backend endpoints
|
||||
;;
|
||||
@ -94,7 +94,7 @@
|
||||
(if code (str code ": " msg) msg))))
|
||||
(str/join " <- ")))
|
||||
|
||||
(defn- fetch!
|
||||
(defn- fetch
|
||||
"`undici/fetch` that fails with an ex-info carrying the target uri and the
|
||||
unwrapped cause chain, so a failed request says what actually went wrong and
|
||||
against which endpoint."
|
||||
@ -106,7 +106,7 @@
|
||||
cause))))))
|
||||
|
||||
(defn- explain
|
||||
"Log-friendly reason for `cause`: the detail `fetch!` already attached, or a
|
||||
"Log-friendly reason for `cause`: the detail `fetch` already attached, or a
|
||||
freshly unwrapped chain for anything else (WASM aborts, decode errors)."
|
||||
[cause]
|
||||
(or (:detail (ex-data cause))
|
||||
@ -146,7 +146,7 @@
|
||||
:file-id (str file-id)
|
||||
:page-id (str page-id)
|
||||
:roots (count root-ids))
|
||||
(->> (fetch! uri #js {:method "POST" :headers headers :body body})
|
||||
(->> (fetch uri #js {:method "POST" :headers headers :body body})
|
||||
(p/mcat (fn [^js resp]
|
||||
(if (= 200 (.-status resp))
|
||||
(.text resp)
|
||||
@ -177,7 +177,7 @@
|
||||
body (t/encode-str (cond-> {:file-id file-id}
|
||||
share-id (assoc :share-id share-id)))
|
||||
uri (internal-uri "api/rpc/command/get-font-variants")]
|
||||
(->> (fetch! uri #js {:method "POST" :headers headers :body body})
|
||||
(->> (fetch uri #js {:method "POST" :headers headers :body body})
|
||||
(p/mcat (fn [^js resp]
|
||||
(if (= 200 (.-status resp))
|
||||
(.text resp)
|
||||
@ -193,7 +193,7 @@
|
||||
here degrades to fallback fonts, it does not fail the export."
|
||||
([uri] (fetch-ttf-bytes uri #js {:method "GET"}))
|
||||
([uri opts]
|
||||
(->> (fetch! uri opts)
|
||||
(->> (fetch uri opts)
|
||||
(p/mcat (fn [^js resp]
|
||||
(if (= 200 (.-status resp))
|
||||
(.arrayBuffer resp)
|
||||
@ -291,7 +291,7 @@
|
||||
;; Emoji and non-latin scripts render through fallback families, not through
|
||||
;; any span's font family, so `wasm/fonts-for-shape` never reports them and the
|
||||
;; provisioning above never uploads them. Must run per request, since
|
||||
;; `clear-fonts!` empties the store; the TTF bytes stay cached per process.
|
||||
;; `clear-fonts` empties the store; the TTF bytes stay cached per process.
|
||||
|
||||
(defn- scene-fallback-fonts
|
||||
"Fallback font descriptors needed by the scene's text. Deduped because
|
||||
@ -319,7 +319,7 @@
|
||||
(cached-ttf-bytes [font-id weight style] #(fetch-gfont-bytes ttf-url))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn- provision-fallback-fonts!
|
||||
(defn- provision-fallback-fonts
|
||||
[scene]
|
||||
(->> (scene-fallback-fonts scene)
|
||||
(map (fn [{:keys [font-id weight style is-emoji is-fallback] :as font}]
|
||||
@ -327,12 +327,12 @@
|
||||
(->> (fetch-fallback-font-bytes font)
|
||||
(p/fmap (fn [buf]
|
||||
(if buf
|
||||
(wasm/store-font! {:id (uuid/get-u32 font-uuid)
|
||||
:weight weight
|
||||
:style style
|
||||
:emoji? (boolean is-emoji)
|
||||
:fallback? (boolean is-fallback)}
|
||||
buf)
|
||||
(wasm/store-font {:id (uuid/get-u32 font-uuid)
|
||||
:weight weight
|
||||
:style style
|
||||
:emoji? (boolean is-emoji)
|
||||
:fallback? (boolean is-fallback)}
|
||||
buf)
|
||||
(l/warn :hint "wasm render: fallback font unavailable"
|
||||
:font-id font-id)))))
|
||||
(p/resolved nil))))
|
||||
@ -349,7 +349,7 @@
|
||||
[media-id {:keys [token]}]
|
||||
(let [headers (asset-headers token)
|
||||
uri (internal-uri (str "assets/by-file-media-id/" media-id))]
|
||||
(->> (fetch! uri #js {:method "GET" :headers headers})
|
||||
(->> (fetch uri #js {:method "GET" :headers headers})
|
||||
(p/mcat (fn [^js resp]
|
||||
(if (= 200 (.-status resp))
|
||||
(.arrayBuffer resp)
|
||||
@ -365,7 +365,7 @@
|
||||
:detail (explain cause) :cause cause)
|
||||
(p/resolved nil))))))
|
||||
|
||||
(defn- provision-images!
|
||||
(defn- provision-images
|
||||
"Fetches and stores every image the scene references (shape, stroke and
|
||||
text-span fills, enumerated by `app.common.types.shape.images`). Unlike fonts,
|
||||
the image store is not reset per request, so already-held images are skipped
|
||||
@ -380,7 +380,7 @@
|
||||
:total (count all-ids)
|
||||
:cached (- (count all-ids) (count new-ids)))
|
||||
(doseq [image-id all-ids]
|
||||
(wasm/store-image-url! image-id (public-uri (str "assets/by-file-media-id/" image-id))))
|
||||
(wasm/store-image-url image-id (public-uri (str "assets/by-file-media-id/" image-id))))
|
||||
(->> new-ids
|
||||
(map (fn [image-id]
|
||||
(->> (fetch-file-media-bytes image-id params)
|
||||
@ -390,22 +390,22 @@
|
||||
(l/dbg :hint "wasm render: image stored"
|
||||
:media-id (str image-id)
|
||||
:bytes (.-byteLength ^js buf))
|
||||
(wasm/store-image! image-id buf))
|
||||
(wasm/store-image image-id buf))
|
||||
(l/warn :hint "wasm render: image unavailable"
|
||||
:media-id (str image-id))))))))
|
||||
(p/all))))
|
||||
|
||||
(defn- relayout-text!
|
||||
(defn- relayout-text
|
||||
"Recomputes layout for every text shape, once the real fonts are provisioned
|
||||
(serialize-time layout used the fallback)."
|
||||
[scene]
|
||||
(doseq [shape (vals scene)
|
||||
:when (= :text (:type shape))]
|
||||
(wasm/update-text-layout! (:id shape))))
|
||||
(wasm/update-text-layout (:id shape))))
|
||||
|
||||
;; --- render
|
||||
|
||||
(defn- check-cancelled!
|
||||
(defn- check-cancelled
|
||||
"Cancellation is cooperative: a render already inside Skia cannot be
|
||||
interrupted, so the flag is only observed between objects. Killing a job
|
||||
mid-object is the caller's job (terminating the worker)."
|
||||
@ -440,17 +440,17 @@
|
||||
:objects (count objects)
|
||||
:file-id (str (:file-id params))
|
||||
:page-id (str (:page-id params)))
|
||||
(->> (ensure-module!)
|
||||
(->> (ensure-module)
|
||||
(p/mcat (fn [_] (fetch-objects params)))
|
||||
(p/mcat (fn [scene]
|
||||
(l/dbg :hint "wasm render: scene fetched" :shapes (count scene))
|
||||
(serialize/serialize-scene! scene)
|
||||
(serialize/serialize-scene scene)
|
||||
(l/dbg :hint "wasm render: scene serialized")
|
||||
;; So fonts from a previous request don't leak into this one.
|
||||
(wasm/clear-fonts!)
|
||||
(wasm/clear-fonts)
|
||||
(->> (p/all [(fetch-font-variants params)
|
||||
(provision-images! scene params)
|
||||
(provision-fallback-fonts! scene)])
|
||||
(provision-images scene params)
|
||||
(provision-fallback-fonts scene)])
|
||||
(p/mcat
|
||||
(fn [[variants _]]
|
||||
(let [variants (or variants [])
|
||||
@ -459,14 +459,14 @@
|
||||
;; Before rendering, so the relayout below sees real
|
||||
;; font metrics. Deduped across objects: shapes
|
||||
;; sharing one family download its TTF once.
|
||||
(wasm/provision-fonts! (map :id objects) resolve-font
|
||||
:font-url font-url))))
|
||||
(wasm/provision-fonts (map :id objects) resolve-font
|
||||
:font-url font-url))))
|
||||
(p/mcat
|
||||
(fn [_]
|
||||
(relayout-text! scene)
|
||||
(relayout-text scene)
|
||||
(p/run
|
||||
(fn [{:keys [id] :as object}]
|
||||
(check-cancelled! params)
|
||||
(check-cancelled params)
|
||||
(let [bytes (render-object-bytes type id scale)
|
||||
path (sh/tempfile :prefix "penpot.tmp.wasm."
|
||||
:suffix (mime/get-extension type))]
|
||||
@ -481,7 +481,7 @@
|
||||
(p/fmap (fn [result]
|
||||
;; After the request, never mid-render, so an image can't
|
||||
;; disappear under a running export.
|
||||
(let [evicted (wasm/evict-images! (cf/get :wasm-worker-image-cache-size wasm/image-cache-size))]
|
||||
(let [evicted (wasm/evict-images (cf/get :wasm-worker-image-cache-size wasm/image-cache-size))]
|
||||
(when (pos? evicted)
|
||||
(l/info :hint "wasm render: evicted cached images" :count evicted)))
|
||||
result))
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
[app.common.render-wasm.wasm :as wasm]
|
||||
[app.wasm.text :as text]))
|
||||
|
||||
(defn set-shape!
|
||||
(defn set-shape
|
||||
"Serializes a single shape into the WASM design state. The host-independent
|
||||
properties (base props, children, blur, shadows, svg-attrs, mask, bool-type,
|
||||
path geometry, grow-type) go through the shared `serialize-shape!` — the same
|
||||
@ -34,13 +34,13 @@
|
||||
(when-not (= type :group)
|
||||
(props/write-shape-strokes! (:strokes prepared)))
|
||||
(when (= type :text)
|
||||
(text/set-shape-text! (:content prepared)))))
|
||||
(text/set-shape-text (:content prepared)))))
|
||||
|
||||
(defn serialize-scene!
|
||||
(defn serialize-scene
|
||||
"Loads every shape of an `objects` map into the WASM design state. Resets the
|
||||
shapes pool first so repeated exports don't accumulate into the shared
|
||||
state. Order is irrelevant: shapes reference each other by id and the tree
|
||||
is resolved at render time."
|
||||
[objects]
|
||||
(h/call wasm/internal-module "_init_shapes_pool" (count objects))
|
||||
(run! set-shape! (vals objects)))
|
||||
(run! set-shape (vals objects)))
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
[app.common.render-wasm.text-content :as tc]
|
||||
[app.common.render-wasm.wasm :as wasm]))
|
||||
|
||||
(defn set-shape-text!
|
||||
(defn set-shape-text
|
||||
"Serializes a text shape's content into the current WASM shape. Mirrors the
|
||||
editor's sequence: clear -> vertical-align -> append each paragraph -> layout.
|
||||
Byte writing and font resolution are the shared
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
[app.wasm.render :as render]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- post!
|
||||
(defn- post
|
||||
[message]
|
||||
(.postMessage ^js wt/parentPort message))
|
||||
|
||||
@ -41,13 +41,13 @@
|
||||
(assoc :cancelled? (cancelled-fn (unchecked-get data "cancel"))))]
|
||||
(->> (render/render params
|
||||
(fn [object]
|
||||
(post! #js {:type "object" :payload (t/encode-str object)})))
|
||||
(p/fmap (fn [_] (post! #js {:type "done"})))
|
||||
(post #js {:type "object" :payload (t/encode-str object)})))
|
||||
(p/fmap (fn [_] (post #js {:type "done"})))
|
||||
(p/merr (fn [cause]
|
||||
(l/warn :hint "render worker: request failed" :cause cause)
|
||||
(post! #js {:type "error"
|
||||
:message (or (ex-message cause) (str cause))
|
||||
:code (some-> cause ex-data :code name)})
|
||||
(post #js {:type "error"
|
||||
:message (or (ex-message cause) (str cause))
|
||||
:code (some-> cause ex-data :code name)})
|
||||
(p/resolved nil))))))
|
||||
|
||||
(defn- on-message
|
||||
@ -67,5 +67,5 @@
|
||||
(defn main
|
||||
[& _]
|
||||
@listening
|
||||
(post! #js {:type "ready"})
|
||||
(post #js {:type "ready"})
|
||||
(l/info :hint "render worker ready"))
|
||||
|
||||
@ -41,11 +41,11 @@
|
||||
(p/resolved #js {:status 403
|
||||
:text (constantly (p/resolved "{}"))}))
|
||||
|
||||
(defn- install-fetch!
|
||||
(defn- install-fetch
|
||||
"Put a fake in place and return the thunk that puts the original
|
||||
back. The thunk belongs at the end of the promise chain's `p/do`, so
|
||||
the fake covers the whole async run."
|
||||
([] (install-fetch! fake-fetch))
|
||||
([] (install-fetch fake-fetch))
|
||||
([the-fake]
|
||||
(set! api/fetch the-fake)
|
||||
(reset! calls [])
|
||||
@ -78,13 +78,13 @@
|
||||
"Run `thunk`, then put the original fetch back, then say the test is
|
||||
finished: the rest never waits on the assertions, so a broken chain
|
||||
reports a failure, never a hang."
|
||||
[thunk restore! done]
|
||||
(p/do thunk (restore!) (done)))
|
||||
[thunk restore-fetch done]
|
||||
(p/do thunk (restore-fetch) (done)))
|
||||
|
||||
(t/deftest claim-job-posts-the-claim-of-one-poller
|
||||
(t/async done
|
||||
(let [job-id (uuid/next)
|
||||
restore! (install-fetch!)]
|
||||
restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/fmap (fn [_]
|
||||
(t/is (str/ends-with? (last-uri) "/api/management/methods/claim-job"))
|
||||
@ -92,20 +92,20 @@
|
||||
(t/is (= {:job-id job-id :scheduled-at "2026-10-07T00:00:00Z"}
|
||||
(decode-body))))
|
||||
(api/claim-job job-id "2026-10-07T00:00:00Z"))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest json-calls-declare-the-transit-content-type
|
||||
;; without it the backend never parses the body as transit and
|
||||
;; validates an empty map (every key a `missing-key`)
|
||||
(t/async done
|
||||
(let [restore! (install-fetch!)]
|
||||
(let [restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/fmap (fn [_]
|
||||
(t/is (= "application/transit+json"
|
||||
(fetch-header "Content-Type"))))
|
||||
(api/claim-job (uuid/next) "2026-10-07T00:00:00Z"))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest report-job-progress-posts-the-beat
|
||||
@ -113,13 +113,13 @@
|
||||
(let [job-id (uuid/next)
|
||||
progress {:stage :rendering
|
||||
:counters {:objects {:current 1 :total 2}}}
|
||||
restore! (install-fetch!)]
|
||||
restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/fmap (fn [_]
|
||||
(t/is (str/ends-with? (last-uri) "/api/management/methods/report-job-progress"))
|
||||
(t/is (= {:job-id job-id :progress progress} (decode-body))))
|
||||
(api/report-job-progress job-id progress))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest fail-job-carries-the-error-and-the-session
|
||||
@ -127,7 +127,7 @@
|
||||
(let [job-id (uuid/next)
|
||||
sid (uuid/next)
|
||||
error {:type :internal :code :processing-error :hint "boom"}
|
||||
restore! (install-fetch!)]
|
||||
restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/mcat (fn [_]
|
||||
(p/fmap (fn [_]
|
||||
@ -135,14 +135,14 @@
|
||||
(decode-body))))
|
||||
(api/fail-job job-id error :session-id sid)))
|
||||
(api/fail-job job-id error))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest complete-job-posts-the-plain-result
|
||||
(t/async done
|
||||
(let [job-id (uuid/next)
|
||||
sid (uuid/next)
|
||||
restore! (install-fetch!)]
|
||||
restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/mcat (fn [_]
|
||||
(p/fmap (fn [_]
|
||||
@ -150,7 +150,7 @@
|
||||
(decode-body))))
|
||||
(api/complete-job job-id {:total 1} {:session-id sid})))
|
||||
(api/complete-job job-id {:total 1}))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest complete-job-with-artifact-rides-multipart
|
||||
@ -158,7 +158,7 @@
|
||||
(let [job-id (uuid/next)
|
||||
sid (uuid/next)
|
||||
tmpfile (path/join sh/tmpdir (str "test-artifact." (uuid/next)))
|
||||
restore! (install-fetch!)]
|
||||
restore-fetch (install-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/mcat (fn [_]
|
||||
(p/fmap (fn [_]
|
||||
@ -176,12 +176,12 @@
|
||||
:mtype "application/zip"})))
|
||||
(->> (fsp/writeFile tmpfile "the export bytes")
|
||||
(p/fmap (fn [_] nil))))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
(t/deftest a-failed-management-answer-is-the-failure-of-the-call
|
||||
(t/async done
|
||||
(let [restore! (install-fetch! failure-fetch)]
|
||||
(let [restore-fetch (install-fetch failure-fetch)]
|
||||
(with-restore-and-done
|
||||
(p/catch (fn [error]
|
||||
(let [data (ex-data error)]
|
||||
@ -189,5 +189,5 @@
|
||||
(t/is (= :failed-management-request (:code data)))
|
||||
(t/is (= 403 (:status data)))))
|
||||
(api/claim-job (uuid/next) "2026-10-07T00:00:00Z"))
|
||||
restore!
|
||||
restore-fetch
|
||||
done))))
|
||||
|
||||
@ -13,16 +13,16 @@
|
||||
[app.jobs :as jobs]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- register!
|
||||
(defn- register
|
||||
[]
|
||||
(let [job-id (uuid/next)]
|
||||
(jobs/register! job-id)
|
||||
(jobs/register job-id)
|
||||
job-id))
|
||||
|
||||
(t/deftest mark-cancelled-runs-the-cancel-pieces-once
|
||||
(t/testing "the signal arms, the callbacks run, and a second mark is a no-op"
|
||||
(t/async done
|
||||
(let [job-id (register!)
|
||||
(let [job-id (register)
|
||||
seen (atom [])]
|
||||
(jobs/on-cancel job-id (fn [] (swap! seen conj :first)))
|
||||
(jobs/on-cancel job-id (fn [] (swap! seen conj :second)))
|
||||
@ -46,7 +46,7 @@
|
||||
;; terminate of the worker is what a hard-cancel is for, and the pieces
|
||||
;; below it must not race the writer
|
||||
(t/async done
|
||||
(let [job-id (register!)
|
||||
(let [job-id (register)
|
||||
seen (atom ::not-called)]
|
||||
(jobs/on-cancel job-id (fn [] (reset! seen (jobs/cancelled? job-id))))
|
||||
(jobs/mark-cancelled job-id)
|
||||
@ -57,7 +57,7 @@
|
||||
(t/testing "a job cancelled before its render leased a worker is
|
||||
already stoppable for the render that comes next"
|
||||
(t/async done
|
||||
(let [job-id (register!)]
|
||||
(let [job-id (register)]
|
||||
(jobs/mark-cancelled job-id)
|
||||
(let [^js signal (jobs/cancel-signal job-id)]
|
||||
(t/is (some? signal))
|
||||
@ -68,10 +68,10 @@
|
||||
(t/testing "a late cancel for a settled job changes nothing, and
|
||||
release drops every piece"
|
||||
(t/async done
|
||||
(let [job-id (register!)
|
||||
(let [job-id (register)
|
||||
seen (atom ::not-called)]
|
||||
(jobs/on-cancel job-id (fn [] (reset! seen :callback-ran)))
|
||||
(jobs/release! job-id)
|
||||
(jobs/release job-id)
|
||||
|
||||
(t/is (nil? (jobs/mark-cancelled job-id)))
|
||||
(t/is (= ::not-called @seen))
|
||||
|
||||
@ -58,10 +58,10 @@
|
||||
|
||||
(deftest exporter-honours-svg-attr-fills
|
||||
(async done
|
||||
(->> (wasm-io/init!)
|
||||
(->> (wasm-io/init)
|
||||
(p/mcat
|
||||
(fn [_]
|
||||
(serialize/serialize-scene! (scene))
|
||||
(serialize/serialize-scene (scene))
|
||||
(let [red (render-svg-string svg-red-id)
|
||||
blue (render-svg-string blue-id)]
|
||||
(is (str/includes? blue "blue")
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
"Log level example:\n"
|
||||
" node target/tests/test.js --focus exporter-tests.renderer-svg-test --log-level warn"))
|
||||
|
||||
(defn- fail!
|
||||
(defn- fail
|
||||
[message]
|
||||
(js/console.error message)
|
||||
(.exit js/process 1))
|
||||
@ -93,7 +93,7 @@
|
||||
(let [[ns-name test-name & extra] (str/split focus #"/")]
|
||||
(cond
|
||||
(or (str/blank? ns-name) (seq extra))
|
||||
(fail! (str "Invalid --focus value: " focus))
|
||||
(fail (str "Invalid --focus value: " focus))
|
||||
|
||||
(some? test-name)
|
||||
{:ns (symbol ns-name) :test test-name}
|
||||
@ -129,17 +129,17 @@
|
||||
(defn- selected-tests
|
||||
[{:keys [ns test]}]
|
||||
(when-not (some #{ns} test-namespaces)
|
||||
(fail! (str "Unknown test namespace: " ns)))
|
||||
(fail (str "Unknown test namespace: " ns)))
|
||||
(let [vars (vec (ns-test-vars ns))]
|
||||
(when (empty? vars)
|
||||
(fail! (str "No tests found in namespace: " ns)))
|
||||
(fail (str "No tests found in namespace: " ns)))
|
||||
(if test
|
||||
(let [test-sym (symbol test)
|
||||
test-var (some #(when (= test-sym (:name (meta %))) %) vars)]
|
||||
(if test-var
|
||||
{:vars [test-var]
|
||||
:fixtures (ns-fixtures ns [test-var])}
|
||||
(fail! (str "Unknown test var: " ns "/" test))))
|
||||
(fail (str "Unknown test var: " ns "/" test))))
|
||||
{:vars vars
|
||||
:fixtures (ns-fixtures ns vars)})))
|
||||
|
||||
@ -148,7 +148,7 @@
|
||||
{:once (apply merge (keep :once fixtures))
|
||||
:each (apply merge (keep :each fixtures))})
|
||||
|
||||
(defn- run-test-vars!
|
||||
(defn- run-test-vars
|
||||
[tests]
|
||||
(let [vars (vec (mapcat :vars tests))
|
||||
fixtures (merge-fixtures (map :fixtures tests))
|
||||
@ -167,16 +167,16 @@
|
||||
(t/report @summary)
|
||||
(t/report (assoc @summary :type :end-run-tests)))]))))
|
||||
|
||||
(defn- run-focused-test!
|
||||
(defn- run-focused-test
|
||||
[focus]
|
||||
(run-test-vars! [(selected-tests (parse-focus focus))]))
|
||||
(run-test-vars [(selected-tests (parse-focus focus))]))
|
||||
|
||||
(defn -main
|
||||
[]
|
||||
(let [{:keys [options errors summary]} (parse-opts (argv) cli-options)]
|
||||
(cond
|
||||
(seq errors)
|
||||
(fail! (str/join "\n" errors))
|
||||
(fail (str/join "\n" errors))
|
||||
|
||||
(:help options)
|
||||
(do
|
||||
@ -187,5 +187,5 @@
|
||||
(do
|
||||
(l/setup! {:app (or (:log-level options) :warn)})
|
||||
(if (:focus options)
|
||||
(run-focused-test! (:focus options))
|
||||
(run-test-vars! (map #(selected-tests {:ns %}) test-namespaces)))))))
|
||||
(run-focused-test (:focus options))
|
||||
(run-test-vars (map #(selected-tests {:ns %}) test-namespaces)))))))
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
[cljs.test :as t :include-macros true]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- stub-pool!
|
||||
(defn- stub-pool
|
||||
"Installs a pool whose acquire/release/destroy only count calls."
|
||||
[]
|
||||
(let [calls (atom {:acquired 0 :released 0 :destroyed 0})]
|
||||
@ -24,7 +24,7 @@
|
||||
|
||||
(t/deftest releases-the-worker-when-the-body-succeeds
|
||||
(t/async done
|
||||
(let [calls (stub-pool!)]
|
||||
(let [calls (stub-pool)]
|
||||
(p/let [result (pool/with-worker (fn [_] (p/resolved :ok)))]
|
||||
(t/is (= :ok result))
|
||||
(t/is (= 1 (:acquired @calls)))
|
||||
@ -36,7 +36,7 @@
|
||||
(t/deftest gives-the-worker-back-when-the-body-throws-synchronously
|
||||
(t/testing "a raise out of the scope body must not leave the worker borrowed"
|
||||
(t/async done
|
||||
(let [calls (stub-pool!)]
|
||||
(let [calls (stub-pool)]
|
||||
(->> (pool/with-worker (fn [_] (throw (ex-info "cancelled" {}))))
|
||||
(p/hmap (fn [_ cause]
|
||||
(t/is (some? cause))
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
(when (= m method) body))
|
||||
@calls))
|
||||
|
||||
(defn- fake-fetch!
|
||||
(defn- fake-fetch
|
||||
"Installs a management http fake: `respond` answers with the `:action`
|
||||
envelope per [method, body] (the multipart arrives as a FormData
|
||||
instance). Every call lands in `calls` before the answer. Returns the
|
||||
@ -75,11 +75,11 @@
|
||||
|
||||
(def ^:private original-render rd/render)
|
||||
|
||||
(defn- fake-render!
|
||||
(defn- fake-render
|
||||
"Installs a fake renderer that writes one file of `content` per
|
||||
object and calls back with it. When `captured` is an atom, it also
|
||||
keeps the params of the last render. Returns the restore thunk."
|
||||
([content] (fake-render! content nil))
|
||||
([content] (fake-render content nil))
|
||||
([content captured]
|
||||
(let [original rd/render]
|
||||
(set! rd/render
|
||||
@ -94,7 +94,7 @@
|
||||
(p/resolved nil))))
|
||||
(fn [] (set! rd/render original)))))
|
||||
|
||||
(defn- fake-render-fail!
|
||||
(defn- fake-render-fail
|
||||
"A renderer that always fails: the failure path of the run."
|
||||
[]
|
||||
(let [original rd/render]
|
||||
@ -104,7 +104,7 @@
|
||||
|
||||
;; ---- THE QUEUE FAKE
|
||||
|
||||
(defn- fake-conn!
|
||||
(defn- fake-conn
|
||||
"A fake ioredis connection: `blpop` pops one payload from the queue
|
||||
the atom holds, or nil."
|
||||
[queue]
|
||||
@ -150,7 +150,7 @@
|
||||
{:action :run})))
|
||||
|
||||
(defn- run-claim
|
||||
"The claim payload of one call of `process!`."
|
||||
"The claim payload of one call of `process`."
|
||||
[job-id]
|
||||
{:job-id job-id
|
||||
:scheduled-at "2026-10-07T09:00:00Z"})
|
||||
@ -160,12 +160,12 @@
|
||||
(let [job-id (uuid/next)]
|
||||
(t/is (= {:job-id (str job-id)
|
||||
:scheduled-at "2026-10-07T09:00:00Z"}
|
||||
(worker/read-payload! (encoded-claim job-id
|
||||
"2026-10-07T09:00:00Z"))))))
|
||||
(worker/read-payload (encoded-claim job-id
|
||||
"2026-10-07T09:00:00Z"))))))
|
||||
|
||||
|
||||
(t/testing "a corrupt payload is dropped, not thrown at the loop"
|
||||
(t/is (nil? (worker/read-payload! "{not a payload")))))
|
||||
(t/is (nil? (worker/read-payload "{not a payload")))))
|
||||
|
||||
(t/deftest process-hands-the-renderer-uuids-not-claim-strings
|
||||
;; the claim carries the job row JSON: every id a string. The render
|
||||
@ -186,10 +186,10 @@
|
||||
:name "the export"}
|
||||
stages (volatile! [])
|
||||
captured (atom nil)
|
||||
respond! (fake-fetch! (answer-with params sid stages))
|
||||
restore! (fake-render! "rendered!" captured)]
|
||||
(p/let [_ (worker/process! {:job-id (str job-id)
|
||||
:scheduled-at "2026-10-07T09:00:00Z"})]
|
||||
fetch-restore (fake-fetch (answer-with params sid stages))
|
||||
render-restore (fake-render "rendered!" captured)]
|
||||
(p/let [_ (worker/process {:job-id (str job-id)
|
||||
:scheduled-at "2026-10-07T09:00:00Z"})]
|
||||
(let [render-params @captured]
|
||||
(t/testing "every id arrived as a uuid object"
|
||||
(t/is (uuid? (:file-id render-params)))
|
||||
@ -199,52 +199,52 @@
|
||||
(t/is (uuid? (-> render-params :objects first :id))))
|
||||
(t/testing "and the job-id traveled with the render"
|
||||
(t/is (= job-id (:job-id render-params)))))
|
||||
(respond!)
|
||||
(restore!)
|
||||
(fetch-restore)
|
||||
(render-restore)
|
||||
(done)))))
|
||||
|
||||
(t/deftest poll-once-awaits-what-it-claims
|
||||
(t/async done
|
||||
(let [queue (atom [(encoded-claim (uuid/next) "2026-10-07T09:00:00Z")])
|
||||
conn (fake-conn! queue)
|
||||
conn (fake-conn queue)
|
||||
handled (atom [])]
|
||||
(p/let [_ (worker/poll-once! conn
|
||||
"the.queue.key"
|
||||
(fn [claim] (swap! handled conj claim)))]
|
||||
(p/let [_ (worker/poll-once conn
|
||||
"the.queue.key"
|
||||
(fn [claim] (swap! handled conj claim)))]
|
||||
(t/is (= 1 (count @handled)))
|
||||
(done)))))
|
||||
|
||||
(t/deftest poll-once-drops-empty-pops-and-corrupt-payloads
|
||||
(t/async done
|
||||
(let [queue (atom ["{corrupt"])
|
||||
conn (fake-conn! queue)]
|
||||
(p/let [_ (worker/poll-once! conn
|
||||
"the.queue.key"
|
||||
(fn [_claim]
|
||||
(t/is false "corrupt must not be handled")))
|
||||
_ (worker/poll-once! conn
|
||||
"the.queue.key"
|
||||
(fn [_claim]
|
||||
(t/is false "a corrupt pop has nothing to handle")))]
|
||||
conn (fake-conn queue)]
|
||||
(p/let [_ (worker/poll-once conn
|
||||
"the.queue.key"
|
||||
(fn [_claim]
|
||||
(t/is false "corrupt must not be handled")))
|
||||
_ (worker/poll-once conn
|
||||
"the.queue.key"
|
||||
(fn [_claim]
|
||||
(t/is false "a corrupt pop has nothing to handle")))]
|
||||
(t/is (empty? @queue))
|
||||
(done)))))
|
||||
|
||||
(t/deftest process-drops-a-claim-the-worker-never-won
|
||||
(t/async done
|
||||
(let [respond! (fake-fetch!
|
||||
(fn [method _body]
|
||||
(case method
|
||||
"claim-job" {:action :skip :status "cancelled"}
|
||||
{:action :run})))
|
||||
render! (fake-render! "should never render")]
|
||||
(p/let [settled (worker/process!
|
||||
(let [fetch-restore (fake-fetch
|
||||
(fn [method _body]
|
||||
(case method
|
||||
"claim-job" {:action :skip :status "cancelled"}
|
||||
{:action :run})))
|
||||
render-restore (fake-render "should never render")]
|
||||
(p/let [settled (worker/process
|
||||
{:job-id (uuid/next)
|
||||
:scheduled-at "2026-10-07T09:00:00Z"})]
|
||||
(t/testing "only the claim traveled: no session, no report, no render"
|
||||
(t/is (= ["claim-job"] (steps-of)))
|
||||
(t/is (nil? settled)))
|
||||
(respond!)
|
||||
(render!)
|
||||
(fetch-restore)
|
||||
(render-restore)
|
||||
(done)))))
|
||||
|
||||
(t/deftest process-runs-the-claimed-export-to-a-multipart-settle
|
||||
@ -253,9 +253,9 @@
|
||||
sid (uuid/next)
|
||||
params (export-params)
|
||||
stages (volatile! [])
|
||||
respond! (fake-fetch! (answer-with params sid stages))
|
||||
restore! (fake-render! "rendered!")]
|
||||
(p/let [_ (worker/process! (run-claim job-id))]
|
||||
fetch-restore (fake-fetch (answer-with params sid stages))
|
||||
render-restore (fake-render "rendered!")]
|
||||
(p/let [_ (worker/process (run-claim job-id))]
|
||||
(t/testing "the run went by claim, first breath, session, beats, settle"
|
||||
(t/is (= ["claim-job" "report-job-progress" "create-job-session"
|
||||
"report-job-progress" "report-job-progress"
|
||||
@ -269,8 +269,8 @@
|
||||
(t/is (= "image/png" (.get fd "mtype")))))
|
||||
(t/testing "the beats painted the vocabulary in order"
|
||||
(t/is (= [:preparing :rendering :packaging] @stages)))
|
||||
(respond!)
|
||||
(restore!)
|
||||
(fetch-restore)
|
||||
(render-restore)
|
||||
(done)))))
|
||||
|
||||
(t/deftest cancelled-is-a-normal-ending-not-a-failure
|
||||
@ -291,14 +291,14 @@
|
||||
sid (uuid/next)
|
||||
params (export-params)
|
||||
stages (volatile! [])
|
||||
respond! (fake-fetch! (answer-with params sid stages))
|
||||
restore! (fake-render-fail!)]
|
||||
(p/let [_ (worker/process! (run-claim job-id))
|
||||
fetch-restore (fake-fetch (answer-with params sid stages))
|
||||
render-restore (fake-render-fail)]
|
||||
(p/let [_ (worker/process (run-claim job-id))
|
||||
_ (p/delay 1400)]
|
||||
(t/testing "no beat traveled after the fail settle"
|
||||
(t/is (= "fail-job" (last (steps-of)))))
|
||||
(respond!)
|
||||
(restore!)
|
||||
(fetch-restore)
|
||||
(render-restore)
|
||||
(done)))))
|
||||
|
||||
(t/deftest process-settles-a-failed-export-with-fail-job
|
||||
@ -307,9 +307,9 @@
|
||||
sid (uuid/next)
|
||||
params (export-params)
|
||||
stages (volatile! [])
|
||||
respond! (fake-fetch! (answer-with params sid stages))
|
||||
restore! (fake-render-fail!)]
|
||||
(p/let [_ (worker/process! (run-claim job-id))]
|
||||
fetch-restore (fake-fetch (answer-with params sid stages))
|
||||
render-restore (fake-render-fail)]
|
||||
(p/let [_ (worker/process (run-claim job-id))]
|
||||
(t/testing "the settle was a fail, not a complete"
|
||||
(t/is (= "fail-job" (last (steps-of)))))
|
||||
(t/testing "the failure is the shape the backend stores"
|
||||
@ -318,8 +318,8 @@
|
||||
(t/is (= :export-failed (get-in fail [:error :code])))
|
||||
(t/is (= "render boom" (get-in fail [:error :hint])))
|
||||
(t/is (= sid (:session-id fail)))))
|
||||
(respond!)
|
||||
(restore!)
|
||||
(fetch-restore)
|
||||
(render-restore)
|
||||
(done)))))
|
||||
|
||||
;; ---- THE CANCEL
|
||||
@ -335,21 +335,21 @@
|
||||
params (export-params)
|
||||
stages (volatile! [])
|
||||
port (atom 0)
|
||||
respond! (fake-fetch!
|
||||
(fn [method body]
|
||||
(when (= "report-job-progress" method)
|
||||
(vswap! stages conj (get-in body [:progress :stage])))
|
||||
(case method
|
||||
"claim-job" {:action :run :name "export-assets" :params params}
|
||||
"create-job-session" {:session-id sid :session-token "session-token"}
|
||||
"report-job-progress" {:action :skip}
|
||||
{:action :run})))]
|
||||
(p/let [_ (worker/process! (run-claim job-id))]
|
||||
fetch-restore (fake-fetch
|
||||
(fn [method body]
|
||||
(when (= "report-job-progress" method)
|
||||
(vswap! stages conj (get-in body [:progress :stage])))
|
||||
(case method
|
||||
"claim-job" {:action :run :name "export-assets" :params params}
|
||||
"create-job-session" {:session-id sid :session-token "session-token"}
|
||||
"report-job-progress" {:action :skip}
|
||||
{:action :run})))]
|
||||
(p/let [_ (worker/process (run-claim job-id))]
|
||||
(t/testing "the settle of a cancelled job was a fail the backend answers skip"
|
||||
(t/is (= "fail-job" (last (steps-of))))
|
||||
(let [fail (call-of "fail-job")]
|
||||
(t/is (= :job-cancelled (get-in fail [:error :code])))))
|
||||
(respond!)
|
||||
(fetch-restore)
|
||||
(done)))))
|
||||
|
||||
(t/deftest cancel-kills-the-mid-skia-render
|
||||
@ -362,20 +362,19 @@
|
||||
params (export-params)
|
||||
stages (volatile! [])
|
||||
terminated (atom 0)
|
||||
mark! (atom nil)
|
||||
respond! (fake-fetch!
|
||||
(fn [method body]
|
||||
(when (= "report-job-progress" method)
|
||||
(vswap! stages conj (get-in body [:progress :stage])))
|
||||
(case method
|
||||
"claim-job" {:action :run :name "export-assets" :params params}
|
||||
"create-job-session" {:session-id sid :session-token "session-token"}
|
||||
;; the first beat runs, everything else skips:
|
||||
;; the cancel lands right after the claim
|
||||
"report-job-progress" (if (empty? @stages)
|
||||
{:action :run}
|
||||
{:action :skip})
|
||||
{:action :run})))
|
||||
fetch-restore (fake-fetch
|
||||
(fn [method body]
|
||||
(when (= "report-job-progress" method)
|
||||
(vswap! stages conj (get-in body [:progress :stage])))
|
||||
(case method
|
||||
"claim-job" {:action :run :name "export-assets" :params params}
|
||||
"create-job-session" {:session-id sid :session-token "session-token"}
|
||||
;; the first beat runs, everything else skips:
|
||||
;; the cancel lands right after the claim
|
||||
"report-job-progress" (if (empty? @stages)
|
||||
{:action :run}
|
||||
{:action :skip})
|
||||
{:action :run})))
|
||||
render-restore (let [original rd/render]
|
||||
(set! rd/render
|
||||
(fn [_params _on-object]
|
||||
@ -385,9 +384,9 @@
|
||||
;; does not exist in this fake
|
||||
(p/delay 30000)))
|
||||
(fn [] (set! rd/render original)))]
|
||||
(p/let [_ (worker/process! (run-claim job-id))]
|
||||
(p/let [_ (worker/process (run-claim job-id))]
|
||||
(t/testing "the run ended in a fail job, not a complete"
|
||||
(t/is (= "fail-job" (last (steps-of)))))
|
||||
(render-restore)
|
||||
(respond!)
|
||||
(fetch-restore)
|
||||
(done)))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user