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

    stanza: Separate shared state and transport
---
 lisp/jabber-sm-runtime.el |   2 +-
 lisp/jabber-stanza.el     | 138 ++++++++++++++++++++++++++++++++++++
 lisp/jabber-state.el      |  72 +++++++++++++++++++
 lisp/jabber-util.el       | 175 +---------------------------------------------
 4 files changed, 213 insertions(+), 174 deletions(-)

diff --git a/lisp/jabber-sm-runtime.el b/lisp/jabber-sm-runtime.el
index bc0e2cc56b..a08c67c3ba 100644
--- a/lisp/jabber-sm-runtime.el
+++ b/lisp/jabber-sm-runtime.el
@@ -20,7 +20,7 @@
 
 (require 'fsm)
 (require 'jabber-sm)
-(require 'jabber-util)
+(require 'jabber-stanza)
 
 (defun jabber-sm--count-inbound (jc state-data stanza)
   "Record inbound STANZA and send a periodic acknowledgement when due.
diff --git a/lisp/jabber-stanza.el b/lisp/jabber-stanza.el
new file mode 100644
index 0000000000..d36f1f0654
--- /dev/null
+++ b/lisp/jabber-stanza.el
@@ -0,0 +1,138 @@
+;;; jabber-stanza.el --- Jabber stanza transport  -*- 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:
+
+;; Serialize, transmit, and dispatch XMPP stanzas.  Stream Management state
+;; transformations remain in `jabber-sm'; this module applies them at the
+;; network boundary.
+
+;;; Code:
+
+(require 'fsm)
+(require 'jabber-sm)
+(require 'jabber-state)
+(require 'jabber-xml)
+
+(defconst jabber-bind-xmlns "urn:ietf:params:xml:ns:xmpp-bind"
+  "RFC 6120 resource binding namespace.")
+
+(defconst jabber-session-xmlns "urn:ietf:params:xml:ns:xmpp-session"
+  "RFC 6120 session establishment namespace.")
+
+(defconst jabber-streams-xmlns "http://etherx.jabber.org/streams";
+  "RFC 6120 XMPP streams namespace.")
+
+(defvar jabber-debug-log-xml)           ; jabber-console.el
+(declare-function jabber-process-console "jabber-console" (jc direction 
xml-data))
+
+(defun jabber-log-xml (jc direction data)
+  "Log DATA for JC in DIRECTION when XML debugging is enabled."
+  (when jabber-debug-log-xml
+    (jabber-process-console jc direction data)))
+
+(defun jabber-stanza--connection-jid (state-data)
+  "Return the full JID represented by STATE-DATA."
+  (concat (plist-get state-data :username) "@"
+          (plist-get state-data :server) "/"
+          (plist-get state-data :resource)))
+
+(defun jabber-send-string (jc string)
+  "Send STRING through connection JC."
+  (let* ((state-data (fsm-get-state-data jc))
+         (connection (plist-get state-data :connection))
+         (send-function (plist-get state-data :send-function)))
+    (unless connection
+      (error "%s has no connection"
+             (jabber-stanza--connection-jid state-data)))
+    (funcall send-function connection string)))
+
+(defun jabber-send-sexp--raw (jc sexp)
+  "Send SEXP to JC without updating Stream Management state."
+  (condition-case err
+      (jabber-log-xml jc "sending" sexp)
+    (error
+     (ding)
+     (message "Couldn't write XML log: %s" (error-message-string err))
+     (sit-for 2)))
+  (let* ((xml (jabber-sexp2xml sexp))
+         (state-data (fsm-get-state-data jc))
+         (sm-countable (and (plist-get state-data :sm-enabled)
+                            (jabber-sm--stanza-p sexp))))
+    (jabber-send-string
+     jc
+     (if sm-countable
+         (concat xml (jabber-sm--make-request-xml))
+       xml))))
+
+(defun jabber-send-sexp--immediate (jc sexp)
+  "Send SEXP to JC immediately and update Stream Management state."
+  (jabber-send-sexp--raw jc sexp)
+  (jabber-sm--count-outbound (fsm-get-state-data jc) sexp))
+
+(defun jabber-send-sexp (jc sexp)
+  "Send SEXP to JC, respecting Stream Management back-pressure."
+  (let ((state-data (fsm-get-state-data jc)))
+    (if (jabber-sm--should-queue-p state-data sexp)
+        (progn
+          (jabber-sm--enqueue-pending state-data sexp)
+          (when (eq (jabber-xml-node-name sexp) 'message)
+            (message "SM: message queued (waiting for server ack, %d pending)"
+                     (length (plist-get state-data :sm-pending-queue)))))
+      (jabber-send-sexp--immediate jc sexp))))
+
+(defun jabber-send-sexp-if-connected (jc sexp)
+  "Send SEXP through JC only after its session is established."
+  (fsm-send-sync jc (cons :send-if-connected sexp)))
+
+(defvar jabber-use-sasl)                ; jabber-core.el
+
+(defsubst jabber-have-sasl-p ()
+  "Return non-nil when the SASL library is available."
+  (featurep 'sasl))
+
+(defun jabber-send-stream-header (jc)
+  "Send the opening XML stream header to JC."
+  (let* ((state-data (fsm-get-state-data jc))
+         (header
+          (concat "<?xml version='1.0'?><stream:stream to='"
+                  (plist-get state-data :server)
+                  "' xmlns='jabber:client' 
xmlns:stream='http://etherx.jabber.org/streams'"
+                  (if (and (jabber-have-sasl-p) jabber-use-sasl)
+                      " version='1.0'"
+                    "")
+                  ">\n")))
+    (jabber-log-xml jc "sending" header)
+    (jabber-send-string jc header)))
+
+(defvar jabber-xml-data)                ; jabber.el
+
+(defun jabber-process-input (jc xml-data)
+  "Dispatch XML-DATA received on JC through its stanza handler chain."
+  (let* ((jabber-xml-data xml-data)
+         (tag (jabber-xml-node-name xml-data))
+         (handlers (pcase tag
+                     ('iq jabber-iq-chain)
+                     ('presence jabber-presence-chain)
+                     ('message jabber-message-chain))))
+    (dolist (entry handlers)
+      (let ((handler (if (consp entry) (cdr entry) entry)))
+        (condition-case err
+            (funcall handler jc xml-data)
+          ((debug error)
+           (fsm-debug-output "Error %S while processing %S with function %s"
+                             err xml-data handler)))))))
+
+(provide 'jabber-stanza)
+
+;;; jabber-stanza.el ends here
diff --git a/lisp/jabber-state.el b/lisp/jabber-state.el
new file mode 100644
index 0000000000..2720c8f12b
--- /dev/null
+++ b/lisp/jabber-state.el
@@ -0,0 +1,72 @@
+;;; jabber-state.el --- Shared Jabber state  -*- 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:
+
+;; State shared by the connection FSM and protocol handlers lives here.  This
+;; module has no network, database, or user-interface effects.
+
+;;; Code:
+
+(require 'cl-lib)
+
+(defvar jabber-connections nil
+  "List of Jabber connection FSMs.")
+
+(define-obsolete-variable-alias '*jabber-roster*
+  'jabber-roster-list "0.11.0")
+(defvar jabber-roster-list nil
+  "The roster list.")
+
+(defvar jabber-jid-obarray (make-vector 127 0)
+  "Obarray for interned JIDs.")
+
+(define-obsolete-variable-alias '*jabber-disconnecting*
+  'jabber-disconnecting "0.11.0")
+(defvar jabber-disconnecting nil
+  "Non-nil while voluntarily disconnecting.")
+
+(defvar jabber-message-chain nil
+  "Ordered handlers for incoming message stanzas.")
+
+(defvar jabber-iq-chain nil
+  "Ordered handlers for incoming IQ stanzas.")
+
+(defvar jabber-presence-chain nil
+  "Ordered handlers for incoming presence stanzas.")
+
+;;;###autoload
+(defun jabber-chain-add (chain-var handler &optional depth)
+  "Add HANDLER to CHAIN-VAR at numeric priority DEPTH.
+Lower depths run first.  Bare function entries from older versions are
+accepted when checking for an existing handler."
+  (let ((entry (cons (or depth 0) handler))
+        (entry-depth (lambda (item) (if (consp item) (car item) 0)))
+        (entry-function (lambda (item) (if (consp item) (cdr item) item))))
+    (unless (cl-find handler (symbol-value chain-var) :key entry-function)
+      (set chain-var
+           (sort (cons entry (symbol-value chain-var))
+                 (lambda (a b)
+                   (< (funcall entry-depth a)
+                      (funcall entry-depth b))))))))
+
+(defun jabber-clear-roster ()
+  "Clear all interned roster state."
+  (mapatoms (lambda (jid)
+              (unintern jid jabber-jid-obarray))
+            jabber-jid-obarray)
+  (setq jabber-roster-list nil))
+
+(provide 'jabber-state)
+
+;;; jabber-state.el ends here
diff --git a/lisp/jabber-util.el b/lisp/jabber-util.el
index b5619f9220..78d6eb4929 100644
--- a/lisp/jabber-util.el
+++ b/lisp/jabber-util.el
@@ -30,8 +30,9 @@
 ;;; Code:
 
 (require 'cl-lib)
+(require 'jabber-state)
+(require 'jabber-stanza)
 (require 'jabber-xml)
-(require 'jabber-sm)
 (require 'fsm)
 (require 'password-cache)
 
@@ -895,177 +896,5 @@ Matches the \"[LABEL: could not decrypt]\" bodies 
produced by
 message text with a placeholder."
   (and body (string-match-p "\\`\\[.*: could not decrypt\\]\\'" body)))
 
-;;; Connection state primitives
-
-(defconst jabber-bind-xmlns "urn:ietf:params:xml:ns:xmpp-bind"
-  "RFC 6120 resource binding namespace.")
-
-(defconst jabber-session-xmlns "urn:ietf:params:xml:ns:xmpp-session"
-  "RFC 6120 session establishment namespace.")
-
-(defconst jabber-streams-xmlns "http://etherx.jabber.org/streams";
-  "RFC 6120 XMPP streams namespace.")
-
-(defvar jabber-connections nil
-  "List of jabber-connection FSMs.")
-
-(define-obsolete-variable-alias '*jabber-roster*
-  'jabber-roster-list "0.11.0")
-(defvar jabber-roster-list nil
-  "The roster list.")
-
-(defvar jabber-jid-obarray (make-vector 127 0)
-  "Obarray for keeping JIDs.")
-
-(define-obsolete-variable-alias '*jabber-disconnecting*
-  'jabber-disconnecting "0.11.0")
-(defvar jabber-disconnecting nil
-  "Non-nil if are we in the process of voluntary disconnection.")
-
-(defvar jabber-message-chain nil
-  "Incoming messages are sent to these functions, in order.
-Each entry is a cons (DEPTH . HANDLER).  Lower depth runs first.")
-
-(defvar jabber-iq-chain nil
-  "Incoming infoqueries are sent to these functions, in order.
-Each entry is a cons (DEPTH . HANDLER).  Lower depth runs first.")
-
-(defvar jabber-presence-chain nil
-  "Incoming presence notifications are sent to these functions, in order.
-Each entry is a cons (DEPTH . HANDLER).  Lower depth runs first.")
-
-;;;###autoload
-(defun jabber-chain-add (chain-var handler &optional depth)
-  "Add HANDLER to stanza processing chain CHAIN-VAR.
-DEPTH is numeric priority (default 0).  Lower runs first.
-Tolerates bare function entries from old-style registrations."
-  (let ((entry (cons (or depth 0) handler))
-        (entry-depth (lambda (e) (if (consp e) (car e) 0)))
-        (entry-fn (lambda (e) (if (consp e) (cdr e) e))))
-    (unless (cl-find handler (symbol-value chain-var) :key entry-fn)
-      (set chain-var
-           (sort (cons entry (symbol-value chain-var))
-                 (lambda (a b)
-                   (< (funcall entry-depth a)
-                      (funcall entry-depth b))))))))
-
-;;; Stanza transmission
-
-(defvar jabber-debug-log-xml)           ; jabber-console.el
-(declare-function jabber-process-console "jabber-console" (jc direction 
xml-data))
-
-(defun jabber-log-xml (fsm direction data)
-  "Print DATA to XML console.
-If `jabber-debug-log-xml' is nil, do nothing.
-FSM is the connection that is sending/receiving.
-DIRECTION is a string, either \"sending\" or \"receive\".
-DATA is any sexp."
-  (when jabber-debug-log-xml
-    (jabber-process-console fsm direction data)))
-
-(defun jabber-send-sexp--raw (jc sexp)
-  "Send SEXP to JC without updating Stream Management state.
-Log the XML and append an <r/> request when SM is enabled for a
-countable stanza."
-  (condition-case e
-      (jabber-log-xml jc "sending" sexp)
-    (error
-     (ding)
-     (message "Couldn't write XML log: %s" (error-message-string e))
-     (sit-for 2)))
-  (let* ((xml (jabber-sexp2xml sexp))
-         (state-data (fsm-get-state-data jc))
-         (sm-countable (and (plist-get state-data :sm-enabled)
-                            (jabber-sm--stanza-p sexp))))
-    (if sm-countable
-        (jabber-send-string jc (concat xml (jabber-sm--make-request-xml)))
-      (jabber-send-string jc xml))))
-
-(defun jabber-send-sexp--immediate (jc sexp)
-  "Send SEXP to JC immediately, bypassing the back-pressure gate.
-Update Stream Management state after transmitting the stanza."
-  (jabber-send-sexp--raw jc sexp)
-  (jabber-sm--count-outbound (fsm-get-state-data jc) sexp))
-
-(defun jabber-send-sexp (jc sexp)
-  "Send the xml corresponding to SEXP to connection JC.
-When SM back-pressure is active and the in-flight limit is reached,
-queue the stanza for later delivery instead of sending immediately."
-  (let ((state-data (fsm-get-state-data jc)))
-    (if (jabber-sm--should-queue-p state-data sexp)
-        (progn
-          (jabber-sm--enqueue-pending state-data sexp)
-          (when (eq (jabber-xml-node-name sexp) 'message)
-            (message "SM: message queued (waiting for server ack, %d pending)"
-                     (length (plist-get state-data :sm-pending-queue)))))
-      (jabber-send-sexp--immediate jc sexp))))
-
-(defun jabber-send-sexp-if-connected (jc sexp)
-  "Send the stanza SEXP only if JC has established a session."
-  (fsm-send-sync jc (cons :send-if-connected sexp)))
-
-(defun jabber-send-string (jc string)
-  "Send STRING through the connection JC."
-  (let* ((state-data (fsm-get-state-data jc))
-         (connection (plist-get state-data :connection))
-         (send-function (plist-get state-data :send-function)))
-    (unless connection
-      (error "%s has no connection" (jabber-connection-jid jc)))
-    (funcall send-function connection string)))
-
-(defun jabber-send-stream-header (jc)
-  "Send stream header to connection JC."
-  (let ((stream-header
-         (concat "<?xml version='1.0'?><stream:stream to='"
-                (plist-get (fsm-get-state-data jc) :server)
-                "' xmlns='jabber:client' 
xmlns:stream='http://etherx.jabber.org/streams'"
-                ;; Not supporting SASL is not XMPP compliant,
-                ;; so don't pretend we are.
-                (if (and (jabber-have-sasl-p) jabber-use-sasl)
-                    " version='1.0'"
-                  "")
-                ">
-")))
-    (jabber-log-xml jc "sending" stream-header)
-    (jabber-send-string jc stream-header)))
-
-;;; Stanza dispatch
-
-(defvar jabber-xml-data)                ; jabber.el
-
-(defun jabber-process-input (jc xml-data)
-  "Process an incoming parsed tag.
-
-JC is the Jabber connection.
-XML-DATA is the parsed tree data from the stream (stanzas)
-obtained from `xml-parse-region'."
-  (let* ((jabber-xml-data xml-data)
-         (tag (jabber-xml-node-name xml-data))
-        (functions (pcase tag
-                     ('iq jabber-iq-chain)
-                     ('presence jabber-presence-chain)
-                     ('message jabber-message-chain))))
-    (dolist (entry functions)
-      (let ((f (if (consp entry) (cdr entry) entry)))
-        (condition-case e
-           (funcall f jc xml-data)
-         ((debug error)
-          (fsm-debug-output "Error %S while processing %S with function %s" e 
xml-data f)))))))
-
-(defun jabber-clear-roster ()
-  "Clean up the roster."
-  (mapatoms (lambda (x)
-             (unintern x jabber-jid-obarray))
-           jabber-jid-obarray)
-  (setq jabber-roster-list nil))
-
-;;; SASL check
-
-(defvar jabber-use-sasl)                ; jabber-core.el
-
-(defsubst jabber-have-sasl-p ()
-  "Return non-nil if SASL library is available."
-  (featurep 'sasl))
-
 (provide 'jabber-util)
 ;;; jabber-util.el ends here

Reply via email to