Some testing and fixing receiving messages over SSE
This commit is contained in:
+2
-2
@@ -55,8 +55,8 @@ Processes the envelope and updates UI."
|
|||||||
(with-current-buffer buf
|
(with-current-buffer buf
|
||||||
(noise-chat-list--render))))))
|
(noise-chat-list--render))))))
|
||||||
|
|
||||||
;; Override the default handler in noise-sse
|
;; Register the receive handler with the SSE client.
|
||||||
(setq noise-sse--handle-event #'noise-receive--handle-sse-event)
|
(setq noise-sse--event-handler #'noise-receive--handle-sse-event)
|
||||||
|
|
||||||
;;;###autoload
|
;;;###autoload
|
||||||
(defun noise-receive-start-sse ()
|
(defun noise-receive-start-sse ()
|
||||||
|
|||||||
+103
-19
@@ -39,38 +39,118 @@
|
|||||||
"Parse an SSE EVENT-STRING into a hash table.
|
"Parse an SSE EVENT-STRING into a hash table.
|
||||||
Returns nil for empty or invalid events."
|
Returns nil for empty or invalid events."
|
||||||
(when (and event-string (not (string-empty-p event-string)))
|
(when (and event-string (not (string-empty-p event-string)))
|
||||||
(let ((lines (split-string event-string "\n"))
|
(let ((lines (split-string event-string "\r?\n"))
|
||||||
(data-lines '()))
|
(data-lines '()))
|
||||||
(dolist (line lines)
|
(dolist (line lines)
|
||||||
(when (string-prefix-p "data: " line)
|
(when (string-match "\\`data:\\(?: \\)?\\(.*\\)\\'" line)
|
||||||
(push (substring line 6) data-lines)))
|
(push (match-string 1 line) data-lines)))
|
||||||
(when data-lines
|
(when data-lines
|
||||||
(let ((json-str (mapconcat #'identity (nreverse data-lines) "")))
|
(let ((json-str (mapconcat #'identity (nreverse data-lines) "\n")))
|
||||||
(condition-case nil
|
(condition-case nil
|
||||||
(json-parse-string json-str)
|
(json-parse-string json-str)
|
||||||
(error nil)))))))
|
(error nil)))))))
|
||||||
|
|
||||||
(defvar noise-sse--buffer ""
|
(defvar noise-sse--buffer ""
|
||||||
"Buffer for partial SSE event data.")
|
"Decoded SSE event data awaiting a complete event delimiter.")
|
||||||
|
|
||||||
|
(defvar noise-sse--http-buffer ""
|
||||||
|
"Raw HTTP response data buffered until headers are consumed.")
|
||||||
|
|
||||||
|
(defvar noise-sse--chunk-buffer ""
|
||||||
|
"Raw chunked HTTP body data buffered until a whole chunk arrives.")
|
||||||
|
|
||||||
|
(defvar noise-sse--headers-received nil
|
||||||
|
"Non-nil once the HTTP response headers have been consumed.")
|
||||||
|
|
||||||
|
(defvar noise-sse--chunked-response-p nil
|
||||||
|
"Non-nil when the SSE response uses chunked transfer encoding.")
|
||||||
|
|
||||||
|
(defun noise-sse--dispatch-events (string)
|
||||||
|
"Append STRING to the SSE event buffer and dispatch complete events."
|
||||||
|
(setq noise-sse--buffer (concat noise-sse--buffer string))
|
||||||
|
(while (string-match "\r?\n\r?\n" noise-sse--buffer)
|
||||||
|
(let ((event-end (match-beginning 0))
|
||||||
|
(event-next (match-end 0)))
|
||||||
|
(let ((event-str (substring noise-sse--buffer 0 event-end)))
|
||||||
|
(setq noise-sse--buffer (substring noise-sse--buffer event-next))
|
||||||
|
(when-let ((event (noise-sse--parse-event event-str)))
|
||||||
|
(funcall noise-sse--event-handler event))))))
|
||||||
|
|
||||||
|
(defun noise-sse--decode-chunked (string)
|
||||||
|
"Decode HTTP chunked transfer body STRING.
|
||||||
|
Returns newly decoded SSE payload, buffering incomplete chunks."
|
||||||
|
(setq noise-sse--chunk-buffer (concat noise-sse--chunk-buffer string))
|
||||||
|
(let ((decoded '()))
|
||||||
|
(catch 'stop
|
||||||
|
(while t
|
||||||
|
(unless (string-match "\r?\n" noise-sse--chunk-buffer)
|
||||||
|
(throw 'stop nil))
|
||||||
|
(let* ((line-end (match-beginning 0))
|
||||||
|
(line-next (match-end 0))
|
||||||
|
(size-line (car (split-string
|
||||||
|
(substring noise-sse--chunk-buffer 0 line-end)
|
||||||
|
";" t "[ \t]*"))))
|
||||||
|
(unless (and size-line
|
||||||
|
(string-match-p "\\`[0-9A-Fa-f]+\\'" size-line))
|
||||||
|
(throw 'stop nil))
|
||||||
|
(let* ((size (string-to-number size-line 16))
|
||||||
|
(data-start line-next)
|
||||||
|
(data-end (+ data-start size)))
|
||||||
|
(when (= size 0)
|
||||||
|
(setq noise-sse--chunk-buffer "")
|
||||||
|
(throw 'stop nil))
|
||||||
|
(when (> data-end (length noise-sse--chunk-buffer))
|
||||||
|
(throw 'stop nil))
|
||||||
|
(let ((tail-end
|
||||||
|
(cond
|
||||||
|
((and (<= (+ data-end 2) (length noise-sse--chunk-buffer))
|
||||||
|
(string= "\r\n"
|
||||||
|
(substring noise-sse--chunk-buffer data-end (+ data-end 2))))
|
||||||
|
(+ data-end 2))
|
||||||
|
((and (< data-end (length noise-sse--chunk-buffer))
|
||||||
|
(eq (aref noise-sse--chunk-buffer data-end) ?\n))
|
||||||
|
(1+ data-end))
|
||||||
|
(t
|
||||||
|
(throw 'stop nil)))))
|
||||||
|
(push (substring noise-sse--chunk-buffer data-start data-end) decoded)
|
||||||
|
(setq noise-sse--chunk-buffer
|
||||||
|
(substring noise-sse--chunk-buffer tail-end)))))))
|
||||||
|
(apply #'concat (nreverse decoded))))
|
||||||
|
|
||||||
|
(defun noise-sse--consume-http-body (string)
|
||||||
|
"Consume raw HTTP response STRING and return decoded SSE payload, if any."
|
||||||
|
(let ((body string))
|
||||||
|
(unless noise-sse--headers-received
|
||||||
|
(setq noise-sse--http-buffer (concat noise-sse--http-buffer string))
|
||||||
|
(if (not (string-match "\r?\n\r?\n" noise-sse--http-buffer))
|
||||||
|
(setq body nil)
|
||||||
|
(let ((headers (substring noise-sse--http-buffer 0 (match-beginning 0)))
|
||||||
|
(body-start (match-end 0)))
|
||||||
|
(let ((case-fold-search t))
|
||||||
|
(setq noise-sse--chunked-response-p
|
||||||
|
(string-match-p "\ntransfer-encoding:[^\r\n]*chunked" headers)))
|
||||||
|
(setq body (substring noise-sse--http-buffer body-start)
|
||||||
|
noise-sse--http-buffer ""
|
||||||
|
noise-sse--headers-received t))))
|
||||||
|
(when body
|
||||||
|
(if noise-sse--chunked-response-p
|
||||||
|
(noise-sse--decode-chunked body)
|
||||||
|
body))))
|
||||||
|
|
||||||
(defun noise-sse--filter (proc string)
|
(defun noise-sse--filter (proc string)
|
||||||
"Process filter for SSE connection.
|
"Process filter for SSE connection.
|
||||||
Buffers incoming STRING and processes complete events."
|
Consumes HTTP framing from STRING and dispatches decoded SSE events."
|
||||||
(setq noise-sse--buffer (concat noise-sse--buffer string))
|
(ignore proc)
|
||||||
(while (string-match "\n\n" noise-sse--buffer)
|
(when-let ((body (noise-sse--consume-http-body string)))
|
||||||
(let ((event-end (match-beginning 0))
|
(noise-sse--dispatch-events body)))
|
||||||
(event-start 0))
|
|
||||||
(let ((event-str (substring noise-sse--buffer event-start event-end)))
|
|
||||||
(setq noise-sse--buffer
|
|
||||||
(substring noise-sse--buffer (+ event-end 2)))
|
|
||||||
(when-let ((event (noise-sse--parse-event event-str)))
|
|
||||||
(noise-sse--handle-event event))))))
|
|
||||||
|
|
||||||
(defun noise-sse--handle-event (event)
|
(defun noise-sse--default-handle-event (event)
|
||||||
"Handle a parsed SSE EVENT.
|
"Handle a parsed SSE EVENT when no receiver is installed yet."
|
||||||
This function will be overridden in noise-receive to process envelopes."
|
|
||||||
(message "SSE event received: %s" event))
|
(message "SSE event received: %s" event))
|
||||||
|
|
||||||
|
(defvar noise-sse--event-handler #'noise-sse--default-handle-event
|
||||||
|
"Function called with each parsed SSE event.")
|
||||||
|
|
||||||
(defvar noise-signal-cli-address)
|
(defvar noise-signal-cli-address)
|
||||||
(defvar noise-account)
|
(defvar noise-account)
|
||||||
|
|
||||||
@@ -133,7 +213,11 @@ Handles connection close and triggers reconnection."
|
|||||||
(when noise-sse--reconnect-timer
|
(when noise-sse--reconnect-timer
|
||||||
(cancel-timer noise-sse--reconnect-timer)
|
(cancel-timer noise-sse--reconnect-timer)
|
||||||
(setq noise-sse--reconnect-timer nil))
|
(setq noise-sse--reconnect-timer nil))
|
||||||
(setq noise-sse--buffer "")
|
(setq noise-sse--buffer ""
|
||||||
|
noise-sse--http-buffer ""
|
||||||
|
noise-sse--chunk-buffer ""
|
||||||
|
noise-sse--headers-received nil
|
||||||
|
noise-sse--chunked-response-p nil)
|
||||||
(message "Noise: SSE disconnected"))
|
(message "Noise: SSE disconnected"))
|
||||||
|
|
||||||
(provide 'noise-sse)
|
(provide 'noise-sse)
|
||||||
|
|||||||
@@ -189,3 +189,22 @@
|
|||||||
"timestamp" 1717930000000
|
"timestamp" 1717930000000
|
||||||
"message" "new message here")))
|
"message" "new message here")))
|
||||||
(should (string-match-p "Sarah> new message here" (buffer-string))))))
|
(should (string-match-p "Sarah> new message here" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest noise-receive-handle-sse-event-stores-message ()
|
||||||
|
"An SSE envelope event stores the message and refreshes the open buffer."
|
||||||
|
(noise-receive-test--with-db
|
||||||
|
(noise-db-upsert-conversation "+1" :type "direct" :name "Sarah"
|
||||||
|
:members '("+1"))
|
||||||
|
(require 'noise-conversation)
|
||||||
|
(with-current-buffer (noise-conversation-open "+1")
|
||||||
|
(noise-receive--handle-sse-event
|
||||||
|
(noise-receive-test--ht
|
||||||
|
"envelope"
|
||||||
|
(noise-receive-test--ht
|
||||||
|
"sourceNumber" "+1" "sourceName" "Sarah"
|
||||||
|
"dataMessage" (noise-receive-test--ht
|
||||||
|
"timestamp" 1717930000000
|
||||||
|
"message" "from sse"))))
|
||||||
|
(should (string-match-p "Sarah> from sse" (buffer-string))))
|
||||||
|
(let ((msg (car (noise-db-get-messages "+1"))))
|
||||||
|
(should (string= "from sse" (gethash "body" msg))))))
|
||||||
|
|||||||
+57
-8
@@ -60,24 +60,73 @@
|
|||||||
|
|
||||||
(ert-deftest noise-sse--filter-processes-complete-event ()
|
(ert-deftest noise-sse--filter-processes-complete-event ()
|
||||||
"Process filter parses and calls handler for complete events."
|
"Process filter parses and calls handler for complete events."
|
||||||
(let ((processed-events '()))
|
(let ((processed-events '())
|
||||||
(cl-letf (((symbol-function 'noise-sse--handle-event)
|
(noise-sse--buffer "")
|
||||||
|
(noise-sse--http-buffer "")
|
||||||
|
(noise-sse--chunk-buffer "")
|
||||||
|
(noise-sse--headers-received t)
|
||||||
|
(noise-sse--chunked-response-p nil))
|
||||||
|
(let ((noise-sse--event-handler
|
||||||
(lambda (event) (push event processed-events))))
|
(lambda (event) (push event processed-events))))
|
||||||
(let ((noise-sse--buffer ""))
|
|
||||||
(noise-sse--filter nil "data: {\"type\":\"message\"}\n\n")
|
(noise-sse--filter nil "data: {\"type\":\"message\"}\n\n")
|
||||||
(should (= 1 (length processed-events)))
|
(should (= 1 (length processed-events)))
|
||||||
(should (hash-table-p (car processed-events)))))))
|
(should (hash-table-p (car processed-events))))))
|
||||||
|
|
||||||
(ert-deftest noise-sse--filter-buffers-incomplete-event ()
|
(ert-deftest noise-sse--filter-buffers-incomplete-event ()
|
||||||
"Process filter buffers incomplete events until newline."
|
"Process filter buffers incomplete events until newline."
|
||||||
(let ((processed-events '()))
|
(let ((processed-events '())
|
||||||
(cl-letf (((symbol-function 'noise-sse--handle-event)
|
(noise-sse--buffer "")
|
||||||
|
(noise-sse--http-buffer "")
|
||||||
|
(noise-sse--chunk-buffer "")
|
||||||
|
(noise-sse--headers-received t)
|
||||||
|
(noise-sse--chunked-response-p nil))
|
||||||
|
(let ((noise-sse--event-handler
|
||||||
(lambda (event) (push event processed-events))))
|
(lambda (event) (push event processed-events))))
|
||||||
(let ((noise-sse--buffer ""))
|
|
||||||
(noise-sse--filter nil "data: {\"type\":\"message\"")
|
(noise-sse--filter nil "data: {\"type\":\"message\"")
|
||||||
(should (= 0 (length processed-events)))
|
(should (= 0 (length processed-events)))
|
||||||
(noise-sse--filter nil "}\n\n")
|
(noise-sse--filter nil "}\n\n")
|
||||||
(should (= 1 (length processed-events)))))))
|
(should (= 1 (length processed-events))))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--filter-skips-http-headers ()
|
||||||
|
"HTTP response headers should not block the first SSE event."
|
||||||
|
(let ((processed-events '())
|
||||||
|
(noise-sse--buffer "")
|
||||||
|
(noise-sse--http-buffer "")
|
||||||
|
(noise-sse--chunk-buffer "")
|
||||||
|
(noise-sse--headers-received nil)
|
||||||
|
(noise-sse--chunked-response-p nil))
|
||||||
|
(let ((noise-sse--event-handler
|
||||||
|
(lambda (event) (push event processed-events))))
|
||||||
|
(noise-sse--filter nil
|
||||||
|
(concat "HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Type: text/event-stream\r\n\r\n"
|
||||||
|
"data: {\"type\":\"message\"}\n\n"))
|
||||||
|
(should (= 1 (length processed-events)))
|
||||||
|
(should (equal "message" (gethash "type" (car processed-events)))))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--filter-decodes-chunked-http-body ()
|
||||||
|
"Chunked HTTP SSE responses should be decoded before parsing events."
|
||||||
|
(let ((processed-events '())
|
||||||
|
(noise-sse--buffer "")
|
||||||
|
(noise-sse--http-buffer "")
|
||||||
|
(noise-sse--chunk-buffer "")
|
||||||
|
(noise-sse--headers-received nil)
|
||||||
|
(noise-sse--chunked-response-p nil))
|
||||||
|
(let ((noise-sse--event-handler
|
||||||
|
(lambda (event) (push event processed-events))))
|
||||||
|
(noise-sse--filter nil
|
||||||
|
(concat "HTTP/1.1 200 OK\r\n"
|
||||||
|
"Transfer-Encoding: chunked\r\n"
|
||||||
|
"Content-Type: text/event-stream\r\n\r\n"
|
||||||
|
"12\r\n"
|
||||||
|
"data: {\"type\":\"msg"
|
||||||
|
"\r\n"
|
||||||
|
"4\r\n"
|
||||||
|
"\"}\n\n"
|
||||||
|
"\r\n"
|
||||||
|
"0\r\n\r\n"))
|
||||||
|
(should (= 1 (length processed-events)))
|
||||||
|
(should (equal "msg" (gethash "type" (car processed-events)))))))
|
||||||
|
|
||||||
(ert-deftest noise-sse--build-url ()
|
(ert-deftest noise-sse--build-url ()
|
||||||
"Build SSE endpoint URL from base address."
|
"Build SSE endpoint URL from base address."
|
||||||
|
|||||||
Reference in New Issue
Block a user