Force full terminal frame every 5 seconds for client resync
Clients can get stuck in outdated state due to dropped packets or missed WebSocket updates. Now forces a full frame refresh every 5 seconds instead of only sending diffs, ensuring clients resync. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,12 @@
|
|||||||
|
|
||||||
;; Cache storing terminal state per session
|
;; Cache storing terminal state per session
|
||||||
;; Key: session-name (string)
|
;; Key: session-name (string)
|
||||||
;; Value: {:lines [vector of strings] :hash int :frame-id long}
|
;; Value: {:lines [vector of strings] :hash int :frame-id long :last-full-ms long}
|
||||||
(defonce ^:private terminal-cache (ConcurrentHashMap.))
|
(defonce ^:private terminal-cache (ConcurrentHashMap.))
|
||||||
|
|
||||||
|
;; Interval for forcing full frame refresh (5 seconds)
|
||||||
|
(def ^:private full-frame-interval-ms 5000)
|
||||||
|
|
||||||
;; Global frame counter - auto-incrementing sequence for ordering frames
|
;; Global frame counter - auto-incrementing sequence for ordering frames
|
||||||
;; Uses AtomicLong for thread safety. Max value is 2^53-1 (JS MAX_SAFE_INTEGER)
|
;; Uses AtomicLong for thread safety. Max value is 2^53-1 (JS MAX_SAFE_INTEGER)
|
||||||
;; to ensure safe handling on the client side
|
;; to ensure safe handling on the client side
|
||||||
@@ -71,6 +74,13 @@
|
|||||||
(< (/ change-count max-count) 0.5))
|
(< (/ change-count max-count) 0.5))
|
||||||
diff)))))
|
diff)))))
|
||||||
|
|
||||||
|
(defn- should-force-full-frame?
|
||||||
|
"Check if enough time has passed since last full frame to force a new one.
|
||||||
|
This helps resync clients that may have missed updates."
|
||||||
|
[cached]
|
||||||
|
(when-let [last-full-ms (:last-full-ms cached)]
|
||||||
|
(> (- (System/currentTimeMillis) last-full-ms) full-frame-interval-ms)))
|
||||||
|
|
||||||
(defn- compute-diff
|
(defn- compute-diff
|
||||||
"Compute diff between cached state and new content.
|
"Compute diff between cached state and new content.
|
||||||
|
|
||||||
@@ -80,11 +90,14 @@
|
|||||||
- {:type :full :lines [lines] :total-lines n :frame-id n} - full refresh needed
|
- {:type :full :lines [lines] :total-lines n :frame-id n} - full refresh needed
|
||||||
|
|
||||||
All responses include :frame-id for ordering. Unchanged responses use
|
All responses include :frame-id for ordering. Unchanged responses use
|
||||||
the cached frame-id since content hasn't changed."
|
the cached frame-id since content hasn't changed.
|
||||||
|
|
||||||
|
Forces a full frame every 5 seconds to help resync out-of-sync clients."
|
||||||
[cached new-content]
|
[cached new-content]
|
||||||
(let [new-lines (content->lines new-content)
|
(let [new-lines (content->lines new-content)
|
||||||
new-count (count new-lines)
|
new-count (count new-lines)
|
||||||
new-hash (hash new-content)]
|
new-hash (hash new-content)
|
||||||
|
force-full? (should-force-full-frame? cached)]
|
||||||
(cond
|
(cond
|
||||||
;; No cached state - full refresh
|
;; No cached state - full refresh
|
||||||
(nil? cached)
|
(nil? cached)
|
||||||
@@ -93,33 +106,46 @@
|
|||||||
:lines new-lines
|
:lines new-lines
|
||||||
:total-lines new-count
|
:total-lines new-count
|
||||||
:hash new-hash
|
:hash new-hash
|
||||||
:frame-id frame-id})
|
:frame-id frame-id
|
||||||
|
:forced false})
|
||||||
|
|
||||||
;; Content unchanged (fast path via hash)
|
;; Content unchanged (fast path via hash)
|
||||||
(= (:hash cached) new-hash)
|
;; But still force full frame periodically for resync
|
||||||
|
(and (= (:hash cached) new-hash) (not force-full?))
|
||||||
{:type :unchanged
|
{:type :unchanged
|
||||||
:total-lines new-count
|
:total-lines new-count
|
||||||
:hash new-hash
|
:hash new-hash
|
||||||
:frame-id (:frame-id cached)} ;; Reuse cached frame-id for unchanged
|
:frame-id (:frame-id cached)} ;; Reuse cached frame-id for unchanged
|
||||||
|
|
||||||
;; Compute line diff
|
;; Force full frame for resync even if unchanged
|
||||||
|
(and (= (:hash cached) new-hash) force-full?)
|
||||||
|
(let [frame-id (next-frame-id)]
|
||||||
|
{:type :full
|
||||||
|
:lines new-lines
|
||||||
|
:total-lines new-count
|
||||||
|
:hash new-hash
|
||||||
|
:frame-id frame-id
|
||||||
|
:forced true})
|
||||||
|
|
||||||
|
;; Compute line diff (unless forcing full)
|
||||||
:else
|
:else
|
||||||
(let [old-lines (:lines cached)
|
(let [old-lines (:lines cached)
|
||||||
line-diff (compute-line-diff old-lines new-lines)
|
line-diff (when-not force-full? (compute-line-diff old-lines new-lines))
|
||||||
frame-id (next-frame-id)]
|
frame-id (next-frame-id)]
|
||||||
(if line-diff
|
(if (and line-diff (not force-full?))
|
||||||
;; Partial diff is efficient
|
;; Partial diff is efficient
|
||||||
{:type :diff
|
{:type :diff
|
||||||
:changes line-diff
|
:changes line-diff
|
||||||
:total-lines new-count
|
:total-lines new-count
|
||||||
:hash new-hash
|
:hash new-hash
|
||||||
:frame-id frame-id}
|
:frame-id frame-id}
|
||||||
;; Too many changes - send full
|
;; Too many changes or forcing full - send full
|
||||||
{:type :full
|
{:type :full
|
||||||
:lines new-lines
|
:lines new-lines
|
||||||
:total-lines new-count
|
:total-lines new-count
|
||||||
:hash new-hash
|
:hash new-hash
|
||||||
:frame-id frame-id})))))
|
:frame-id frame-id
|
||||||
|
:forced force-full?})))))
|
||||||
|
|
||||||
(defn capture-with-diff
|
(defn capture-with-diff
|
||||||
"Capture terminal content and compute diff from cached state.
|
"Capture terminal content and compute diff from cached state.
|
||||||
@@ -135,13 +161,17 @@
|
|||||||
[session-name capture-fn]
|
[session-name capture-fn]
|
||||||
(let [new-content (capture-fn session-name)
|
(let [new-content (capture-fn session-name)
|
||||||
cached (.get terminal-cache session-name)
|
cached (.get terminal-cache session-name)
|
||||||
diff-result (compute-diff cached new-content)]
|
diff-result (compute-diff cached new-content)
|
||||||
;; Update cache if changed (store frame-id for unchanged responses)
|
is-full? (= :full (:type diff-result))
|
||||||
|
now (System/currentTimeMillis)]
|
||||||
|
;; Update cache if changed or full frame sent
|
||||||
(when (not= :unchanged (:type diff-result))
|
(when (not= :unchanged (:type diff-result))
|
||||||
(.put terminal-cache session-name
|
(.put terminal-cache session-name
|
||||||
{:lines (content->lines new-content)
|
{:lines (content->lines new-content)
|
||||||
:hash (:hash diff-result)
|
:hash (:hash diff-result)
|
||||||
:frame-id (:frame-id diff-result)}))
|
:frame-id (:frame-id diff-result)
|
||||||
|
;; Track when last full frame was sent for periodic resync
|
||||||
|
:last-full-ms (if is-full? now (or (:last-full-ms cached) now))}))
|
||||||
{:content new-content
|
{:content new-content
|
||||||
:diff diff-result
|
:diff diff-result
|
||||||
:changed (not= :unchanged (:type diff-result))}))
|
:changed (not= :unchanged (:type diff-result))}))
|
||||||
|
|||||||
Reference in New Issue
Block a user