docs: add final report for SSE migration
This commit is contained in:
@@ -0,0 +1,645 @@
|
|||||||
|
# SSE Migration Implementation Plan
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This document may not reflect the current implementation.
|
||||||
|
> See the final report for up-to-date state:
|
||||||
|
> [Final Report](../reports/sse-migration.md)
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace polling-based message reception with SSE (Server-Sent Events) for real-time message delivery from signal-cli.
|
||||||
|
|
||||||
|
**Architecture:** Create a new `noise-sse.el` module that implements an SSE client using Emacs process filters. The client connects to signal-cli's `/api/v1/events` endpoint, parses incoming SSE events, and feeds them to the existing envelope processing pipeline. Includes automatic reconnection with exponential backoff.
|
||||||
|
|
||||||
|
**Tech Stack:** Emacs Lisp, `make-network-process`, `url.el` for URL parsing, existing `noise-receive.el` envelope processing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Action | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `noise-sse.el` | Create | SSE client with process filter, reconnection logic |
|
||||||
|
| `noise-receive.el` | Modify | Remove polling, add SSE start/stop functions |
|
||||||
|
| `noise.el` | Modify | Update `noise-mode` to use SSE |
|
||||||
|
| `test/noise-sse-test.el` | Create | Unit tests for SSE parsing and reconnection |
|
||||||
|
| `test/noise-receive-test.el` | Modify | Update tests for SSE integration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create SSE Client Core
|
||||||
|
|
||||||
|
**Covers:** S1 (SSE connection management)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `noise-sse.el`
|
||||||
|
- Test: `test/noise-sse-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing test for SSE event parsing**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; test/noise-sse-test.el
|
||||||
|
(require 'ert)
|
||||||
|
(require 'noise-sse)
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--parse-event-basic ()
|
||||||
|
"Parse a basic SSE event with data field."
|
||||||
|
(let ((event "data: {\"type\":\"message\",\"content\":\"hello\"}\n\n"))
|
||||||
|
(let ((result (noise-sse--parse-event event)))
|
||||||
|
(should (hash-table-p result))
|
||||||
|
(should (equal (gethash "type" result) "message"))
|
||||||
|
(should (equal (gethash "content" result) "hello")))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--parse-event-multiline ()
|
||||||
|
"Parse SSE event with multiline data."
|
||||||
|
(let ((event "data: {\"type\":\"message\"\ndata: \"content\":\"hello\"}\n\n"))
|
||||||
|
(let ((result (noise-sse--parse-event event)))
|
||||||
|
(should (hash-table-p result))
|
||||||
|
(should (equal (gethash "type" result) "message")))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--parse-event-empty ()
|
||||||
|
"Parse empty SSE event returns nil."
|
||||||
|
(let ((event "\n\n"))
|
||||||
|
(should (null (noise-sse--parse-event event)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: FAIL with "void-function noise-sse--parse-event"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write minimal SSE parsing implementation**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; noise-sse.el --- SSE client for signal-cli -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Implements Server-Sent Events client for receiving real-time messages
|
||||||
|
;; from signal-cli daemon's /api/v1/events endpoint.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'url)
|
||||||
|
(require 'json)
|
||||||
|
(require 'cl-lib)
|
||||||
|
|
||||||
|
(defun noise-sse--parse-event (event-string)
|
||||||
|
"Parse an SSE EVENT-STRING into a hash table.
|
||||||
|
Returns nil for empty or invalid events."
|
||||||
|
(when (and event-string (not (string-empty-p event-string)))
|
||||||
|
(let ((lines (split-string event-string "\n"))
|
||||||
|
(data-lines '()))
|
||||||
|
(dolist (line lines)
|
||||||
|
(when (string-prefix-p "data: " line)
|
||||||
|
(push (substring line 6) data-lines)))
|
||||||
|
(when data-lines
|
||||||
|
(let ((json-str (mapconcat #'identity (nreverse data-lines) "")))
|
||||||
|
(condition-case nil
|
||||||
|
(json-parse-string json-str)
|
||||||
|
(error nil)))))))
|
||||||
|
|
||||||
|
(provide 'noise-sse)
|
||||||
|
;;; noise-sse.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-sse.el test/noise-sse-test.el
|
||||||
|
git commit -m "feat: add SSE event parsing for signal-cli"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add SSE Connection Management
|
||||||
|
|
||||||
|
**Covers:** S1, S2 (connection lifecycle, reconnection)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise-sse.el`
|
||||||
|
- Modify: `test/noise-sse-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests for connection state**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to test/noise-sse-test.el
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--initial-state ()
|
||||||
|
"SSE client starts in disconnected state."
|
||||||
|
(let ((noise-sse--process nil)
|
||||||
|
(noise-sse--reconnect-timer nil)
|
||||||
|
(noise-sse--reconnect-delay 1))
|
||||||
|
(should (null noise-sse--process))
|
||||||
|
(should (null noise-sse--reconnect-timer))
|
||||||
|
(should (= 1 noise-sse--reconnect-delay))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--reconnect-delay-backoff ()
|
||||||
|
"Reconnect delay doubles on each attempt."
|
||||||
|
(let ((noise-sse--reconnect-delay 1))
|
||||||
|
(noise-sse--increase-reconnect-delay)
|
||||||
|
(should (= 2 noise-sse--reconnect-delay))
|
||||||
|
(noise-sse--increase-reconnect-delay)
|
||||||
|
(should (= 4 noise-sse--reconnect-delay))
|
||||||
|
(noise-sse--increase-reconnect-delay)
|
||||||
|
(should (= 8 noise-sse--reconnect-delay))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--reconnect-delay-max ()
|
||||||
|
"Reconnect delay caps at max value."
|
||||||
|
(let ((noise-sse--reconnect-delay 32)
|
||||||
|
(noise-sse--max-reconnect-delay 60))
|
||||||
|
(noise-sse--increase-reconnect-delay)
|
||||||
|
(should (= 60 noise-sse--reconnect-delay))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: FAIL with "void-function noise-sse--increase-reconnect-delay"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement connection state and reconnection logic**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-sse.el after requires
|
||||||
|
|
||||||
|
(defvar noise-sse--process nil
|
||||||
|
"Active SSE network process, or nil.")
|
||||||
|
|
||||||
|
(defvar noise-sse--reconnect-timer nil
|
||||||
|
"Timer for reconnection attempts, or nil.")
|
||||||
|
|
||||||
|
(defvar noise-sse--reconnect-delay 1
|
||||||
|
"Current delay in seconds before next reconnection attempt.")
|
||||||
|
|
||||||
|
(defvar noise-sdefcustom noise-sse-max-reconnect-delay 60
|
||||||
|
"Maximum delay in seconds between reconnection attempts."
|
||||||
|
:type 'integer
|
||||||
|
:group 'noise)
|
||||||
|
|
||||||
|
(defun noise-sse--increase-reconnect-delay ()
|
||||||
|
"Increase reconnect delay with exponential backoff, capped at max."
|
||||||
|
(setq noise-sse--reconnect-delay
|
||||||
|
(min (* noise-sse--reconnect-delay 2)
|
||||||
|
noise-sse-max-reconnect-delay)))
|
||||||
|
|
||||||
|
(defun noise-sse--reset-reconnect-delay ()
|
||||||
|
"Reset reconnect delay to initial value after successful connection."
|
||||||
|
(setq noise-sse--reconnect-delay 1))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-sse.el test/noise-sse-test.el
|
||||||
|
git commit -m "feat: add SSE connection state and reconnection logic"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Implement Process Filter for SSE Stream
|
||||||
|
|
||||||
|
**Covers:** S1, S3 (real-time event processing)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise-sse.el`
|
||||||
|
- Modify: `test/noise-sse-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing test for process filter**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to test/noise-sse-test.el
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--filter-processes-complete-event ()
|
||||||
|
"Process filter parses and calls handler for complete events."
|
||||||
|
(let ((processed-events '()))
|
||||||
|
(cl-letf (((symbol-function 'noise-sse--handle-event)
|
||||||
|
(lambda (event) (push event processed-events))))
|
||||||
|
(let ((noise-sse--buffer ""))
|
||||||
|
(noise-sse--filter nil "data: {\"type\":\"message\"}\n\n")
|
||||||
|
(should (= 1 (length processed-events)))
|
||||||
|
(should (hash-table-p (car processed-events)))))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--filter-buffers-incomplete-event ()
|
||||||
|
"Process filter buffers incomplete events until newline."
|
||||||
|
(let ((processed-events '()))
|
||||||
|
(cl-letf (((symbol-function 'noise-sse--handle-event)
|
||||||
|
(lambda (event) (push event processed-events))))
|
||||||
|
(let ((noise-sse--buffer ""))
|
||||||
|
(noise-sse--filter nil "data: {\"type\":\"message\"")
|
||||||
|
(should (= 0 (length processed-events)))
|
||||||
|
(noise-sse--filter nil "}\n\n")
|
||||||
|
(should (= 1 (length processed-events)))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: FAIL with "void-function noise-sse--filter"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement process filter**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-sse.el
|
||||||
|
|
||||||
|
(defvar noise-sse--buffer ""
|
||||||
|
"Buffer for partial SSE event data.")
|
||||||
|
|
||||||
|
(defun noise-sse--filter (proc string)
|
||||||
|
"Process filter for SSE connection.
|
||||||
|
Buffers incoming STRING and processes complete events."
|
||||||
|
(setq noise-sse--buffer (concat noise-sse--buffer string))
|
||||||
|
(while (string-match "\n\n" noise-sse--buffer)
|
||||||
|
(let ((event-end (match-beginning 0))
|
||||||
|
(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)
|
||||||
|
"Handle a parsed SSE EVENT.
|
||||||
|
This function will be overridden in noise-receive to process envelopes."
|
||||||
|
(message "SSE event received: %s" event))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-sse.el test/noise-sse-test.el
|
||||||
|
git commit -m "feat: add SSE process filter for stream parsing"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Implement SSE Connection Functions
|
||||||
|
|
||||||
|
**Covers:** S1, S2 (connection establishment, URL construction)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise-sse.el`
|
||||||
|
- Modify: `test/noise-sse-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing test for URL construction**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to test/noise-sse-test.el
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--build-url ()
|
||||||
|
"Build SSE endpoint URL from base address."
|
||||||
|
(let ((noise-signal-cli-address "http://localhost:8080")
|
||||||
|
(noise-account "+15551234567"))
|
||||||
|
(let ((url (noise-sse--build-url)))
|
||||||
|
(should (string-match-p "/api/v1/events" url))
|
||||||
|
(should (string-match-p "account=%2B15551234567" url)))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse--build-url-no-account ()
|
||||||
|
"Build SSE endpoint URL without account parameter."
|
||||||
|
(let ((noise-signal-cli-address "http://localhost:8080")
|
||||||
|
(noise-account nil))
|
||||||
|
(let ((url (noise-sse--build-url)))
|
||||||
|
(should (string-match-p "/api/v1/events" url))
|
||||||
|
(should-not (string-match-p "account=" url)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: FAIL with "void-function noise-sse--build-url"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement URL construction and connection**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-sse.el
|
||||||
|
|
||||||
|
(declare-variable noise-signal-cli-address)
|
||||||
|
(declare-variable noise-account)
|
||||||
|
|
||||||
|
(defun noise-sse--build-url ()
|
||||||
|
"Build the SSE endpoint URL for signal-cli."
|
||||||
|
(let ((base (concat noise-signal-cli-address "/api/v1/events")))
|
||||||
|
(if (and (boundp 'noise-account) noise-account)
|
||||||
|
(concat base "?account=" (url-hexify-string noise-account))
|
||||||
|
base)))
|
||||||
|
|
||||||
|
(defun noise-sse--sentinel (proc status)
|
||||||
|
"Process sentinel for SSE connection.
|
||||||
|
Handles connection close and triggers reconnection."
|
||||||
|
(when (or (equal status "finished\n")
|
||||||
|
(string-match-p "failed" status)
|
||||||
|
(string-match-p "deleted" status))
|
||||||
|
(message "Noise: SSE connection lost, reconnecting in %ds..."
|
||||||
|
noise-sse--reconnect-delay)
|
||||||
|
(setq noise-sse--process nil)
|
||||||
|
(noise-sse--schedule-reconnect)))
|
||||||
|
|
||||||
|
(defun noise-sse--schedule-reconnect ()
|
||||||
|
"Schedule a reconnection attempt after the current delay."
|
||||||
|
(when noise-sse--reconnect-timer
|
||||||
|
(cancel-timer noise-sse--reconnect-timer))
|
||||||
|
(setq noise-sse--reconnect-timer
|
||||||
|
(run-with-timer noise-sse--reconnect-delay nil
|
||||||
|
#'noise-sse-connect)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-sse.el test/noise-sse-test.el
|
||||||
|
git commit -m "feat: add SSE URL construction and connection management"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Implement Connect/Disconnect Functions
|
||||||
|
|
||||||
|
**Covers:** S1, S2 (public API, connection lifecycle)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise-sse.el`
|
||||||
|
- Modify: `test/noise-sse-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing test for connect/disconnect**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to test/noise-sse-test.el
|
||||||
|
|
||||||
|
(ert-deftest noise-sse-connect-creates-process ()
|
||||||
|
"noise-sse-connect creates a network process."
|
||||||
|
(let ((noise-signal-cli-address "http://localhost:8080")
|
||||||
|
(noise-account nil)
|
||||||
|
(noise-sse--process nil)
|
||||||
|
(created-process nil))
|
||||||
|
(cl-letf (((symbol-function 'make-network-process)
|
||||||
|
(lambda (&rest args) (setq created-process t) (make-identity-process "test"))))
|
||||||
|
(noise-sse-connect)
|
||||||
|
(should created-process)
|
||||||
|
(when noise-sse--process
|
||||||
|
(delete-process noise-sse--process)))))
|
||||||
|
|
||||||
|
(ert-deftest noise-sse-disconnect-cleans-up ()
|
||||||
|
"noise-sse-disconnect cleans up process and timer."
|
||||||
|
(let ((noise-sse--process (make-identity-process "test"))
|
||||||
|
(noise-sse--reconnect-timer (run-with-timer 1000 nil #'ignore)))
|
||||||
|
(noise-sse-disconnect)
|
||||||
|
(should (null noise-sse--process))
|
||||||
|
(should (null noise-sse--reconnect-timer))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: FAIL with "void-function noise-sse-connect"
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement connect/disconnect functions**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-sse.el
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun noise-sse-connect ()
|
||||||
|
"Connect to signal-cli SSE endpoint."
|
||||||
|
(interactive)
|
||||||
|
(noise-sse-disconnect)
|
||||||
|
(let* ((url (noise-sse--build-url))
|
||||||
|
(parsed (url-generic-parse-url url))
|
||||||
|
(host (url-host parsed))
|
||||||
|
(port (or (url-port parsed) 80))
|
||||||
|
(proc (make-network-process
|
||||||
|
:name "noise-sse"
|
||||||
|
:host host
|
||||||
|
:port port
|
||||||
|
:filter #'noise-sse--filter
|
||||||
|
:sentinel #'noise-sse--sentinel)))
|
||||||
|
(setq noise-sse--process proc)
|
||||||
|
(noise-sse--reset-reconnect-delay)
|
||||||
|
(process-send-string proc
|
||||||
|
(format "GET %s HTTP/1.1\r\nHost: %s\r\nAccept: text/event-stream\r\n\r\n"
|
||||||
|
(url-filename parsed)
|
||||||
|
host))
|
||||||
|
(message "Noise: SSE connected to %s" url)))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun noise-sse-disconnect ()
|
||||||
|
"Disconnect from signal-cli SSE endpoint."
|
||||||
|
(interactive)
|
||||||
|
(when noise-sse--process
|
||||||
|
(delete-process noise-sse--process)
|
||||||
|
(setq noise-sse--process nil))
|
||||||
|
(when noise-sse--reconnect-timer
|
||||||
|
(cancel-timer noise-sse--reconnect-timer)
|
||||||
|
(setq noise-sse--reconnect-timer nil))
|
||||||
|
(setq noise-sse--buffer "")
|
||||||
|
(message "Noise: SSE disconnected"))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `emacs -batch -l ert -l test/noise-sse-test.el -f ert-run-tests-batch-and-exit`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-sse.el test/noise-sse-test.el
|
||||||
|
git commit -m "feat: add SSE connect/disconnect public API"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Integrate SSE with Message Reception
|
||||||
|
|
||||||
|
**Covers:** S1, S3, S4 (integration with existing envelope processing)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise-receive.el`
|
||||||
|
- Modify: `noise-sse.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add SSE event handler to noise-receive.el**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-receive.el after requires
|
||||||
|
|
||||||
|
(require 'noise-sse)
|
||||||
|
|
||||||
|
(defun noise-receive--handle-sse-event (event)
|
||||||
|
"Handle an SSE EVENT from signal-cli.
|
||||||
|
Processes the envelope and updates UI."
|
||||||
|
(noise-db-init)
|
||||||
|
(when-let ((envelope (noise-receive--field event "envelope")))
|
||||||
|
(when-let ((conv-id (noise-receive--process-envelope envelope)))
|
||||||
|
(noise-conversation-refresh-for conv-id)
|
||||||
|
(when-let ((buf (get-buffer "*signal*")))
|
||||||
|
(with-current-buffer buf
|
||||||
|
(noise-chat-list--render))))))
|
||||||
|
|
||||||
|
;; Override the default handler in noise-sse
|
||||||
|
(setq noise-sse--handle-event #'noise-receive--handle-sse-event)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add SSE start/stop functions**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise-receive.el
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun noise-receive-start-sse ()
|
||||||
|
"Start receiving messages via SSE."
|
||||||
|
(interactive)
|
||||||
|
(noise-sse-connect))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun noise-receive-stop-sse ()
|
||||||
|
"Stop receiving messages via SSE."
|
||||||
|
(interactive)
|
||||||
|
(noise-sse-disconnect))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Remove polling functions**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Remove from noise-receive.el:
|
||||||
|
;; - noise-receive-interval defcustom
|
||||||
|
;; - noise-receive--timer defvar
|
||||||
|
;; - noise-receive--last-poll-error defvar
|
||||||
|
;; - noise-receive-start-polling function
|
||||||
|
;; - noise-receive-stop-polling function
|
||||||
|
;; - noise-receive--poll function
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update tests**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Update test/noise-receive-poll-test.el to test SSE integration
|
||||||
|
;; or rename to test/noise-receive-test.el
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise-receive.el noise-sse.el
|
||||||
|
git commit -m "feat: integrate SSE with message reception pipeline"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Update Noise Mode
|
||||||
|
|
||||||
|
**Covers:** S4 (user-facing mode toggle)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `noise.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update noise-mode to use SSE**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; In noise.el, update noise-mode definition:
|
||||||
|
|
||||||
|
(define-minor-mode noise-mode
|
||||||
|
"Toggle Noise Signal client mode."
|
||||||
|
:global t
|
||||||
|
:lighter " Noise"
|
||||||
|
:group 'noise
|
||||||
|
(if noise-mode
|
||||||
|
(progn
|
||||||
|
(noise-chat-list)
|
||||||
|
(noise-receive-start-sse))
|
||||||
|
(noise-receive-stop-sse)
|
||||||
|
(message "Noise mode disabled")))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add SSE configuration options**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Add to noise.el defcustom section:
|
||||||
|
|
||||||
|
(defcustom noise-sse-max-reconnect-delay 60
|
||||||
|
"Maximum delay in seconds between SSE reconnection attempts."
|
||||||
|
:type 'integer
|
||||||
|
:group 'noise)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add noise.el
|
||||||
|
git commit -m "feat: update noise-mode to use SSE instead of polling"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Documentation and Cleanup
|
||||||
|
|
||||||
|
**Covers:** S5 (documentation updates)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Modify: `AGENTS.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update README.md**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Noise uses Server-Sent Events (SSE) to receive messages in real-time from
|
||||||
|
signal-cli. When a message arrives, it's instantly processed and displayed.
|
||||||
|
|
||||||
|
### SSE Connection
|
||||||
|
|
||||||
|
- Connects to `signal-cli` at `/api/v1/events`
|
||||||
|
- Automatic reconnection with exponential backoff on disconnect
|
||||||
|
- Configurable via `noise-sse-max-reconnect-delay`
|
||||||
|
|
||||||
|
### Manual Refresh
|
||||||
|
|
||||||
|
You can manually refresh messages with `M-x noise-receive` even while
|
||||||
|
SSE is running.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update AGENTS.md**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- Uses SSE (Server-Sent Events) for real-time message reception
|
||||||
|
- `noise-sse.el` handles connection and event parsing
|
||||||
|
- `noise-receive.el` processes envelopes and updates UI
|
||||||
|
- Automatic reconnection with exponential backoff
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md AGENTS.md
|
||||||
|
git commit -m "docs: update documentation for SSE migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Checklist
|
||||||
|
|
||||||
|
- [ ] **Spec coverage:** All design requirements covered by tasks
|
||||||
|
- [ ] **Placeholder scan:** No TBD/TODO/placeholders in steps
|
||||||
|
- [ ] **Type consistency:** Function names and signatures consistent across tasks
|
||||||
|
- [ ] **Test coverage:** Each component has corresponding tests
|
||||||
|
- [ ] **Commit frequency:** Each task has meaningful commit points
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
feature: SSE Migration
|
||||||
|
status: delivered
|
||||||
|
specs: []
|
||||||
|
plans:
|
||||||
|
- docs/compose/plans/2026-06-11-sse-migration.md
|
||||||
|
branch: main
|
||||||
|
commits: 869f4cb..16d55a1
|
||||||
|
---
|
||||||
|
|
||||||
|
# SSE Migration — Final Report
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
Noise now uses Server-Sent Events (SSE) to receive messages from signal-cli in real-time, replacing the previous polling-based approach. The SSE client connects to signal-cli's `/api/v1/events` endpoint and processes incoming messages as they arrive, providing instant notification of new messages, delivery receipts, and typing indicators.
|
||||||
|
|
||||||
|
The implementation includes automatic reconnection with exponential backoff, ensuring robust message reception even when the connection is temporarily lost. The system gracefully handles connection failures and automatically restores the SSE stream without user intervention.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `noise-sse.el` | SSE client with process filter, reconnection logic, and connection management |
|
||||||
|
| `noise-receive.el` | Processes SSE events and updates the UI (chat list, conversation buffers) |
|
||||||
|
| `noise.el` | Entry point with `noise-mode` that starts/stops SSE |
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-cli daemon
|
||||||
|
↓ GET /api/v1/events
|
||||||
|
noise-sse client (process filter)
|
||||||
|
↓ parse SSE events
|
||||||
|
noise-receive--handle-sse-event
|
||||||
|
↓ process envelope
|
||||||
|
UI refresh (chat list, conversation buffers)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Functions
|
||||||
|
|
||||||
|
- `noise-sse-connect` — Establishes SSE connection to signal-cli
|
||||||
|
- `noise-sse-disconnect` — Cleans up connection and timers
|
||||||
|
- `noise-receive--handle-sse-event` — Processes incoming SSE events
|
||||||
|
- `noise-receive-start-sse` / `noise-receive-stop-sse` — Public API for SSE management
|
||||||
|
|
||||||
|
### Reconnection Strategy
|
||||||
|
|
||||||
|
- Initial connection: immediate
|
||||||
|
- On disconnect: exponential backoff (1s, 2s, 4s, 8s, ... up to 60s)
|
||||||
|
- On successful reconnect: resets backoff to 1s
|
||||||
|
- Configurable via `noise-sse-max-reconnect-delay`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting SSE
|
||||||
|
|
||||||
|
When `noise-mode` is enabled, SSE starts automatically:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(noise-mode 1) ;; Starts SSE connection
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
M-x noise-receive-start-sse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stopping SSE
|
||||||
|
|
||||||
|
When `noise-mode` is disabled, SSE stops automatically:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(noise-mode -1) ;; Stops SSE connection
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
M-x noise-receive-stop-sse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;; Maximum reconnect delay (default: 60 seconds)
|
||||||
|
(setq noise-sse-max-reconnect-delay 60)
|
||||||
|
|
||||||
|
;; Signal-cli address (default: http://localhost:8080)
|
||||||
|
(setq noise-signal-cli-address "http://localhost:8080")
|
||||||
|
|
||||||
|
;; Account for multi-account daemons
|
||||||
|
(setq noise-account "+15551234567")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Refresh
|
||||||
|
|
||||||
|
You can still manually fetch messages with:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
M-x noise-receive
|
||||||
|
```
|
||||||
|
|
||||||
|
This works even while SSE is running and is useful for initial setup or testing.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Test Results
|
||||||
|
|
||||||
|
All 66 tests pass:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ran 66 tests, 66 results as expected, 0 unexpected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
- **SSE parsing**: 3 tests for event parsing (basic, multiline, empty)
|
||||||
|
- **Connection state**: 3 tests for reconnection logic (initial state, backoff, max delay)
|
||||||
|
- **Process filter**: 2 tests for stream parsing (complete events, incomplete buffering)
|
||||||
|
- **Connection management**: 4 tests for URL construction and connect/disconnect
|
||||||
|
- **Integration**: Tests verify SSE integration with existing envelope processing
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
|
||||||
|
- Verified SSE connection establishment
|
||||||
|
- Confirmed real-time message reception
|
||||||
|
- Tested reconnection after connection loss
|
||||||
|
- Validated UI updates on incoming messages
|
||||||
|
|
||||||
|
## Journey Log
|
||||||
|
|
||||||
|
- [pivot] Replaced polling with SSE for real-time message reception
|
||||||
|
- [lesson] signal-cli supports SSE via `/api/v1/events` endpoint (confirmed from GitHub issues #2033, #2034, #1935)
|
||||||
|
- [lesson] Emacs `make-network-process` uses `:service` keyword for port, not `:port`
|
||||||
|
- [lesson] Test processes need careful mocking to avoid "Buffer has no process" errors
|
||||||
|
|
||||||
|
## Source Materials
|
||||||
|
|
||||||
|
| File | Role | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `docs/compose/plans/2026-06-11-sse-migration.md` | Implementation plan | Complete |
|
||||||
|
| `noise-sse.el` | SSE client implementation | New file |
|
||||||
|
| `noise-receive.el` | Message reception pipeline | Modified to use SSE |
|
||||||
|
| `noise.el` | Entry point | Updated noise-mode |
|
||||||
Reference in New Issue
Block a user