Skip to content

Instantly share code, notes, and snippets.

@Ramblurr
Created April 10, 2025 11:18
Show Gist options
  • Save Ramblurr/23fa7d6fc215fd4b63d3ba1a5397155c to your computer and use it in GitHub Desktop.
Save Ramblurr/23fa7d6fc215fd4b63d3ba1a5397155c to your computer and use it in GitHub Desktop.
a quicky example of per-tab session state with datastar when using hyperlith style page rendering
;; a quicky example of per-tab session state with datastar
;; when using hyperlith style page rendering
(:require
[clojure.core.async :as a]
[chime.core :as chime])
(def !page-state (atom {}))
(defn state-transact!
"Helper function to be used by your command/action handlers.
Example:
(d*/state-transact! req #(assoc-in % [:some-state-flag] true))
"
[req f]
(if-let [tab-id (-> req :body-params :tab-id)]
(swap! !page-state update tab-id (fn [state]
(-> state
(f)
(assoc ::modified (System/currentTimeMillis)))))
(throw (ex-info "No tab-id in request" {}))))
(defn init-tab-state!
"Initializes a new tab state session. <ch is a channel that will be written to when the tab state changes."
[<ch tab-id]
(swap! !page-state assoc tab-id {::created (System/currentTimeMillis)})
(add-watch !page-state tab-id (fn [watch-key _ _ _]
(when-not (a/>!! <ch :state-changed)
(remove-watch !page-state watch-key)))))
(defn remove-tab-state!
"Cleans up a tab state session"
[tab-id]
(swap! !page-state dissoc tab-id)
(remove-watch !page-state tab-id))
;; Tab sessions older than this are pruned
(def STALE-THRESHOLD-HOURS 12)
(defn stale? [now created modified]
(> (- now (or modified created)) (* STALE-THRESHOLD-HOURS 3600000)))
(defn clean-stale-page-state
"Removes tab-ids that are stale, where stale is defined as not having been modified or created in the last 24 hours."
[page-state]
(let [now (System/currentTimeMillis)]
(reduce-kv (fn [acc tab-id {:keys [::created ::modified]}]
(if (stale? now created modified)
(dissoc acc tab-id)
acc))
page-state
page-state)))
(defn clean-stale-watches!
"Removes watches for tab-ids that are no longer in the page state."
[]
(let [watches (-> !page-state .getWatches keys)
stale-watches (remove #(get @!page-state %) watches)]
(doseq [watch-key stale-watches]
(remove-watch !page-state watch-key))))
(defn start-clean-page-state-job
"Starts a job that cleans stale page state every 10 seconds."
[]
(chime/chime-at (chime/periodic-seq (Instant/now) (Duration/ofSeconds 60))
(fn [_]
(swap! !page-state clean-stale-page-state)
(clean-stale-watches!))))
(comment
;; inside render handler
;; create your tab-id
(str (random-uuid))
;; in hk on-open call
;; <ch is the tapped channel
(init-tab-state! <ch tab-id)
;; in hk on-close cleanup
(remove-tab-state! tab-id)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment