branch: elpa/jabber
commit ca17f4898ad609a5db31777f5bd10ce874159baa
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    sm: Separate runtime effects
---
 lisp/jabber-core.el       |   1 +
 lisp/jabber-sm-runtime.el | 121 +++++++++++++++++++++++++++++++++++++++++++
 lisp/jabber-sm.el         | 127 ++--------------------------------------------
 tests/jabber-test-sm.el   |   1 +
 4 files changed, 126 insertions(+), 124 deletions(-)

diff --git a/lisp/jabber-core.el b/lisp/jabber-core.el
index 945bd8f152..863c644e77 100644
--- a/lisp/jabber-core.el
+++ b/lisp/jabber-core.el
@@ -39,6 +39,7 @@
 (require 'jabber-sasl)
 (require 'jabber-console)
 (require 'jabber-sm)
+(require 'jabber-sm-runtime)
 
 
 (defvar-local jabber-namespace-prefixes nil
diff --git a/lisp/jabber-sm-runtime.el b/lisp/jabber-sm-runtime.el
new file mode 100644
index 0000000000..bc0e2cc56b
--- /dev/null
+++ b/lisp/jabber-sm-runtime.el
@@ -0,0 +1,121 @@
+;;; jabber-sm-runtime.el --- Stream Management effects  -*- lexical-binding: 
t; -*-
+
+;; Copyright (C) 2026  Thanos Apollo
+
+;; Maintainer: Thanos Apollo <[email protected]>
+
+;; This file is a part of jabber.el.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 2 of the License, or
+;; (at your option) any later version.
+
+;;; Commentary:
+
+;; This module contains the network and timer effects for XEP-0198.  State
+;; transitions and XML transformations remain in `jabber-sm'.
+
+;;; Code:
+
+(require 'fsm)
+(require 'jabber-sm)
+(require 'jabber-util)
+
+(defun jabber-sm--count-inbound (jc state-data stanza)
+  "Record inbound STANZA and send a periodic acknowledgement when due.
+JC is the Jabber connection.  Return updated STATE-DATA."
+  (when (and (plist-get state-data :sm-enabled)
+             (jabber-sm--stanza-p stanza))
+    (let ((count (jabber-sm--inc-counter
+                  (plist-get state-data :sm-inbound-count))))
+      (setq state-data
+            (plist-put state-data :sm-inbound-count count))
+      (when (and jabber-sm-ack-interval
+                 (zerop (mod count jabber-sm-ack-interval)))
+        (jabber-sm--send-ack jc state-data))))
+  state-data)
+
+(defun jabber-sm--drain-pending (jc state-data)
+  "Send queued stanzas on JC up to the in-flight limit.
+STATE-DATA is the FSM plist.  Return updated state data."
+  (let ((queue (sort (plist-get state-data :sm-pending-queue)
+                     (lambda (a b) (< (car a) (car b))))))
+    (while (and queue
+                (or (null jabber-sm-max-in-flight)
+                    (< (jabber-sm--in-flight-count state-data)
+                       jabber-sm-max-in-flight)))
+      (let ((sexp (cdr (pop queue))))
+        (jabber-send-sexp--raw jc sexp)
+        (setq state-data (jabber-sm--count-outbound state-data sexp))))
+    (plist-put state-data :sm-pending-queue queue)))
+
+(defun jabber-sm--check-stall (jc)
+  "Check JC for an acknowledgement stall and recover when timed out."
+  (let ((state-data (fsm-get-state-data jc)))
+    (if (and jabber-sm-max-in-flight
+             (plist-get state-data :sm-pending-queue)
+             (>= (jabber-sm--in-flight-count state-data)
+                 jabber-sm-max-in-flight))
+        (let ((stall-since (plist-get state-data :sm-stall-since)))
+          (if stall-since
+              (when (>= (- (float-time) stall-since)
+                        jabber-sm-stall-timeout)
+                (jabber-sm--recover-stall jc state-data))
+            (plist-put state-data :sm-stall-since (float-time))))
+      (plist-put state-data :sm-stall-since nil))))
+
+(defun jabber-sm--recover-stall (jc state-data)
+  "Recover an acknowledgement stall on JC with STATE-DATA."
+  (let ((pending-count (length (plist-get state-data :sm-pending-queue))))
+    (message "SM: ack stall detected, recovering (%d stanzas pending)"
+             pending-count)
+    (plist-put state-data :sm-last-acked
+               (plist-get state-data :sm-outbound-count))
+    (plist-put state-data :sm-outbound-queue nil)
+    (plist-put state-data :sm-stall-since nil)
+    (jabber-sm--drain-pending jc state-data)))
+
+(defun jabber-sm--send-ack (jc state-data)
+  "Send an acknowledgement to JC using STATE-DATA."
+  (jabber-send-string jc (jabber-sm--make-ack-xml
+                          (plist-get state-data :sm-inbound-count))))
+
+(defun jabber-sm--request-ack (jc)
+  "Request an acknowledgement from JC."
+  (jabber-send-string jc (jabber-sm--make-request-xml)))
+
+(defun jabber-sm--r-timer-function (jc)
+  "Request an acknowledgement from JC and check for a stall."
+  (when (memq jc jabber-connections)
+    (condition-case err
+        (progn
+          (jabber-sm--request-ack jc)
+          (jabber-sm--check-stall jc))
+      (error
+       (message "SM: ack timer failed: %s" (error-message-string err))))))
+
+(defun jabber-sm--start-r-timer (jc state-data)
+  "Start the acknowledgement request timer for JC in STATE-DATA."
+  (jabber-sm--stop-r-timer state-data)
+  (let ((timer (run-with-timer jabber-sm-request-interval
+                               jabber-sm-request-interval
+                               #'jabber-sm--r-timer-function jc)))
+    (plist-put state-data :sm-r-timer timer)))
+
+(defun jabber-sm--stop-r-timer (state-data)
+  "Cancel the acknowledgement request timer in STATE-DATA."
+  (let ((timer (plist-get state-data :sm-r-timer)))
+    (when (timerp timer)
+      (cancel-timer timer)))
+  (plist-put state-data :sm-r-timer nil))
+
+(defun jabber-sm-maybe-start (jc)
+  "Start the Stream Management acknowledgement timer for JC when enabled."
+  (let ((state-data (fsm-get-state-data jc)))
+    (when (plist-get state-data :sm-enabled)
+      (jabber-sm--start-r-timer jc state-data))))
+
+(provide 'jabber-sm-runtime)
+
+;;; jabber-sm-runtime.el ends here
diff --git a/lisp/jabber-sm.el b/lisp/jabber-sm.el
index 89ffaaad42..1067935d18 100644
--- a/lisp/jabber-sm.el
+++ b/lisp/jabber-sm.el
@@ -30,9 +30,9 @@
 ;; 2. Stream resumption: fast reconnect that skips SASL auth and
 ;;    preserves the server-side session.
 ;;
-;; SM state is stored on the FSM state-data plist.  All functions in this
-;; file are pure (take state-data, return values) except for the timer
-;; management and the send helpers that call `jabber-send-string'.
+;; SM state is stored on the FSM state-data plist.  This module contains the
+;; state and XML transformations; `jabber-sm-runtime' owns network and timer
+;; effects.
 
 ;;; Code:
 
@@ -40,11 +40,6 @@
 (require 'jabber-xml)
 (require 'fsm)
 
-(declare-function jabber-send-sexp--raw "jabber-util.el" (jc sexp))
-(declare-function jabber-send-string "jabber-util.el" (jc string))
-
-(defvar jabber-debug-log-xml)
-
 (defconst jabber-sm-xmlns "urn:xmpp:sm:3"
   "XEP-0198 Stream Management namespace (version 3).")
 
@@ -181,22 +176,6 @@ Return updated STATE-DATA."
                               (list (cons count sexp)))))))
   state-data)
 
-(defun jabber-sm--count-inbound (jc state-data stanza)
-  "Increment inbound counter if SM is enabled and STANZA is countable.
-When `jabber-sm-ack-interval' is set, send a proactive <a/> every
-that many stanzas.  JC is the Jabber connection.
-Return updated STATE-DATA."
-  (when (and (plist-get state-data :sm-enabled)
-             (jabber-sm--stanza-p stanza))
-    (let ((count (jabber-sm--inc-counter
-                  (plist-get state-data :sm-inbound-count))))
-      (setq state-data
-            (plist-put state-data :sm-inbound-count count))
-      (when (and jabber-sm-ack-interval
-                 (zerop (mod count jabber-sm-ack-interval)))
-        (jabber-sm--send-ack jc state-data))))
-  state-data)
-
 ;;; Back-pressure helpers
 
 (defun jabber-sm--in-flight-count (state-data)
@@ -232,59 +211,6 @@ Return updated STATE-DATA."
              (nconc (plist-get state-data :sm-pending-queue)
                     (list (cons (jabber-sm--stanza-priority sexp) sexp)))))
 
-(defun jabber-sm--drain-pending (jc state-data)
-  "Send queued stanzas from pending queue up to the in-flight cap.
-JC is the connection.  STATE-DATA is the FSM plist.
-The queue is stable-sorted by priority before draining so messages
-go first, then IQs, then presence.  FIFO order is preserved
-within each priority class.
-Return updated STATE-DATA."
-  (let ((queue (sort (plist-get state-data :sm-pending-queue)
-                     (lambda (a b) (< (car a) (car b))))))
-    (while (and queue
-                (or (null jabber-sm-max-in-flight)
-                    (< (jabber-sm--in-flight-count state-data)
-                       jabber-sm-max-in-flight)))
-      (let ((sexp (cdr (pop queue))))
-        (jabber-send-sexp--raw jc sexp)
-        (setq state-data (jabber-sm--count-outbound state-data sexp))))
-    (plist-put state-data :sm-pending-queue queue)))
-
-;;; Stall detection and recovery
-
-(defun jabber-sm--check-stall (jc)
-  "Check for ack stall on JC and recover if timed out.
-Called from the periodic r-timer.  Detects when the back-pressure
-gate is full and the pending queue is non-empty, indicating the
-server has stopped sending ack responses."
-  (let ((state-data (fsm-get-state-data jc)))
-    (if (and jabber-sm-max-in-flight
-             (plist-get state-data :sm-pending-queue)
-             (>= (jabber-sm--in-flight-count state-data)
-                 jabber-sm-max-in-flight))
-        (let ((stall-since (plist-get state-data :sm-stall-since)))
-          (if stall-since
-              (when (>= (- (float-time) stall-since)
-                        jabber-sm-stall-timeout)
-                (jabber-sm--recover-stall jc state-data))
-            (plist-put state-data :sm-stall-since (float-time))))
-      (plist-put state-data :sm-stall-since nil))))
-
-(defun jabber-sm--recover-stall (jc state-data)
-  "Force recovery from an ack stall on JC with STATE-DATA.
-Advance the ack counter to the current outbound count, clear the
-outbound queue, and drain pending stanzas.  The drain is capped by
-`jabber-sm-max-in-flight' so at most that many stanzas go out per
-recovery cycle."
-  (let ((pending-len (length (plist-get state-data :sm-pending-queue))))
-    (message "SM: ack stall detected, recovering (%d stanzas pending)"
-             pending-len)
-    (plist-put state-data :sm-last-acked
-               (plist-get state-data :sm-outbound-count))
-    (plist-put state-data :sm-outbound-queue nil)
-    (plist-put state-data :sm-stall-since nil)
-    (jabber-sm--drain-pending jc state-data)))
-
 ;;; Ack send/receive
 
 (defun jabber-sm--make-ack-xml (h)
@@ -295,15 +221,6 @@ recovery cycle."
   "Return the XML string for <r xmlns='urn:xmpp:sm:3'/>."
   (format "<r xmlns='%s'/>" jabber-sm-xmlns))
 
-(defun jabber-sm--send-ack (jc state-data)
-  "Send an <a/> ack to JC with inbound count from STATE-DATA."
-  (jabber-send-string jc (jabber-sm--make-ack-xml
-                          (plist-get state-data :sm-inbound-count))))
-
-(defun jabber-sm--request-ack (jc)
-  "Send an <r/> request to JC."
-  (jabber-send-string jc (jabber-sm--make-request-xml)))
-
 (defun jabber-sm--prune-queue (queue h)
   "Return QUEUE with entries whose count is <= H removed."
   (cl-remove-if (lambda (entry)
@@ -384,35 +301,6 @@ Return (UPDATED-STATE-DATA . STANZAS-TO-RESEND)."
     (setq state-data (plist-put state-data :sm-resuming nil))
     (cons state-data to-resend)))
 
-;;; Periodic ack request timer
-
-(defun jabber-sm--r-timer-function (jc)
-  "Timer callback: send <r/> to JC and check for ack stall."
-  (when (memq jc jabber-connections)
-    (condition-case err
-        (progn
-          (jabber-sm--request-ack jc)
-          (jabber-sm--check-stall jc))
-      (error
-       (message "SM: ack timer failed: %s" (error-message-string err))))))
-
-(defun jabber-sm--start-r-timer (jc state-data)
-  "Start a periodic <r/> timer for connection JC.
-Return updated STATE-DATA with the timer stored."
-  (jabber-sm--stop-r-timer state-data)
-  (let ((timer (run-with-timer jabber-sm-request-interval
-                               jabber-sm-request-interval
-                               #'jabber-sm--r-timer-function jc)))
-    (plist-put state-data :sm-r-timer timer)))
-
-(defun jabber-sm--stop-r-timer (state-data)
-  "Cancel the periodic <r/> timer if running.
-Return updated STATE-DATA."
-  (let ((timer (plist-get state-data :sm-r-timer)))
-    (when (timerp timer)
-      (cancel-timer timer)))
-  (plist-put state-data :sm-r-timer nil))
-
 ;;; FSM routing helper
 
 (defun jabber-sm--maybe-enable-or-establish (state-data)
@@ -423,15 +311,6 @@ Checks `jabber-sm-enable' and whether stream features 
include SM."
       (list :sm-enable state-data)
     (list :session-established state-data)))
 
-;;; Post-connect hook entry point
-
-(defun jabber-sm-maybe-start (jc)
-  "Start SM ack timer if SM was successfully enabled on JC.
-Intended for `jabber-post-connect-hooks'."
-  (let ((state-data (fsm-get-state-data jc)))
-    (when (plist-get state-data :sm-enabled)
-      (jabber-sm--start-r-timer jc state-data))))
-
 (provide 'jabber-sm)
 
 ;;; jabber-sm.el ends here
diff --git a/tests/jabber-test-sm.el b/tests/jabber-test-sm.el
index ded3883909..5d3d1daec7 100644
--- a/tests/jabber-test-sm.el
+++ b/tests/jabber-test-sm.el
@@ -9,6 +9,7 @@
 (require 'ert)
 (require 'jabber-xml)
 (require 'jabber-sm)
+(require 'jabber-sm-runtime)
 
 (defvar jabber-connections)
 

Reply via email to