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
|
||||
;; 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.))
|
||||
|
||||
;; Interval for forcing full frame refresh (5 seconds)
|
||||
(def ^:private full-frame-interval-ms 5000)
|
||||
|
||||
;; Global frame counter - auto-incrementing sequence for ordering frames
|
||||
;; Uses AtomicLong for thread safety. Max value is 2^53-1 (JS MAX_SAFE_INTEGER)
|
||||
;; to ensure safe handling on the client side
|
||||
@@ -71,6 +74,13 @@
|
||||
(< (/ change-count max-count) 0.5))
|
||||
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
|
||||
"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
|
||||
|
||||
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]
|
||||
(let [new-lines (content->lines new-content)
|
||||
new-count (count new-lines)
|
||||
new-hash (hash new-content)]
|
||||
new-hash (hash new-content)
|
||||
force-full? (should-force-full-frame? cached)]
|
||||
(cond
|
||||
;; No cached state - full refresh
|
||||
(nil? cached)
|
||||
@@ -93,33 +106,46 @@
|
||||
:lines new-lines
|
||||
:total-lines new-count
|
||||
:hash new-hash
|
||||
:frame-id frame-id})
|
||||
:frame-id frame-id
|
||||
:forced false})
|
||||
|
||||
;; 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
|
||||
:total-lines new-count
|
||||
:hash new-hash
|
||||
: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
|
||||
(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)]
|
||||
(if line-diff
|
||||
(if (and line-diff (not force-full?))
|
||||
;; Partial diff is efficient
|
||||
{:type :diff
|
||||
:changes line-diff
|
||||
:total-lines new-count
|
||||
:hash new-hash
|
||||
:frame-id frame-id}
|
||||
;; Too many changes - send full
|
||||
;; Too many changes or forcing full - send full
|
||||
{:type :full
|
||||
:lines new-lines
|
||||
:total-lines new-count
|
||||
:hash new-hash
|
||||
:frame-id frame-id})))))
|
||||
:frame-id frame-id
|
||||
:forced force-full?})))))
|
||||
|
||||
(defn capture-with-diff
|
||||
"Capture terminal content and compute diff from cached state.
|
||||
@@ -135,13 +161,17 @@
|
||||
[session-name capture-fn]
|
||||
(let [new-content (capture-fn session-name)
|
||||
cached (.get terminal-cache session-name)
|
||||
diff-result (compute-diff cached new-content)]
|
||||
;; Update cache if changed (store frame-id for unchanged responses)
|
||||
diff-result (compute-diff cached new-content)
|
||||
is-full? (= :full (:type diff-result))
|
||||
now (System/currentTimeMillis)]
|
||||
;; Update cache if changed or full frame sent
|
||||
(when (not= :unchanged (:type diff-result))
|
||||
(.put terminal-cache session-name
|
||||
{:lines (content->lines new-content)
|
||||
: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
|
||||
:diff diff-result
|
||||
:changed (not= :unchanged (:type diff-result))}))
|
||||
|
||||
Reference in New Issue
Block a user