commit f6e925363cd72b0cdfd3ec5d96ab2faf0e569313
parent dd583bc92c00994ef41b384f40ff8b9da11dec84
Author: Vineet Kumar <git@vineetk.net>
Date: Sat, 7 Mar 2026 17:52:17 -0500
replace url-retrieve with elfeed-curl-enqueue to eliminate blocking
url-retrieve blocks emacs during dns resolution and tls handshake.
elfeed-curl-enqueue runs curl as a subprocess, making fetches fully
non-blocking and mirrors regular elfeed operation.
Also adds Accept header for GitHub API requests.
Diffstat:
2 files changed, 110 insertions(+), 128 deletions(-)
diff --git a/elfeed-custom-feeds-github.el b/elfeed-custom-feeds-github.el
@@ -11,7 +11,7 @@
;;; Code:
(require 'elfeed)
-(require 'url)
+(require 'elfeed-curl)
(require 'json)
(require 'cl-lib)
@@ -44,29 +44,27 @@
(dn display-name))
(run-at-time delay nil
(lambda ()
- (url-retrieve url
- (lambda (status)
- (if (plist-get status :error)
- (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error))
- (goto-char (point-min))
- (re-search-forward "\n\n")
- ;; Capture JSON text for async parsing
- (let ((json-text (buffer-substring-no-properties (point) (point-max))))
- ;; Parse JSON in idle timer to avoid blocking
- (run-with-idle-timer 0 nil
- (lambda ()
- (condition-case err
- (with-temp-buffer
- (insert json-text)
- (goto-char (point-min))
- (let* ((json-object-type 'alist)
- (json-array-type 'list)
- (response (json-read))
- (items (alist-get 'items response)))
- (funcall cb r dn items)))
- (error
- (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err)))))))
- nil t)))))
+ (elfeed-curl-enqueue url
+ (lambda (status)
+ (if (not status)
+ (message "Error fetching GitHub PRs %s [%s]: %s" r dn elfeed-curl-error-message)
+ ;; Capture JSON text for async parsing
+ (let ((json-text (buffer-substring-no-properties (point-min) (point-max))))
+ ;; Parse JSON in idle timer to avoid blocking
+ (run-with-idle-timer 0 nil
+ (lambda ()
+ (condition-case err
+ (with-temp-buffer
+ (insert json-text)
+ (goto-char (point-min))
+ (let* ((json-object-type 'alist)
+ (json-array-type 'list)
+ (response (json-read))
+ (items (alist-get 'items response)))
+ (funcall cb r dn items)))
+ (error
+ (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err))))))))
+ :headers '(("Accept" . "application/vnd.github+json")))))))
(defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback)
"Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME.
@@ -126,7 +124,7 @@ Call COMPLETION-CALLBACK with (repo display-name new-entries) when done."
(run-with-idle-timer 0 nil #'process-next-chunk)
;; All done - call completion callback
(funcall completion-callback repo display-name (nreverse new-entries)))))))
- (process-next-chunk)))))
+ (process-next-chunk))))
;;;###autoload
(defun elfeed-custom-feeds-github-update ()
@@ -138,48 +136,42 @@ Batches all entries and updates UI once at the end to avoid blocking."
(all-new-entries '())
(results '()))
- (when (zerop total)
- (message "No GitHub PR queries configured")
- (cl-return-from elfeed-custom-feeds-github-update nil))
-
- ;; Completion callback that batches everything
- (let ((completion-callback
- (lambda (repo display-name new-entries)
- (run-with-idle-timer 0 nil
- (lambda ()
- (setq processed (1+ processed))
- (when new-entries
- (setq all-new-entries (append all-new-entries new-entries)))
- (push (list repo display-name (length new-entries)) results)
-
- ;; When all queries are done, batch add to DB and update UI once
- (when (= processed total)
- (let ((total-count (length all-new-entries)))
- (when (> total-count 0)
- (run-with-idle-timer 0 nil
+ (if (zerop total)
+ (message "No GitHub PR queries configured")
+ (let ((completion-callback
+ (lambda (repo display-name new-entries)
+ (run-with-idle-timer 0 nil
+ (lambda ()
+ (setq processed (1+ processed))
+ (when new-entries
+ (setq all-new-entries (append all-new-entries new-entries)))
+ (push (list repo display-name (length new-entries)) results)
+ ;; When all queries are done, batch add to DB and update UI once
+ (when (= processed total)
+ (let ((total-count (length all-new-entries)))
+ (when (> total-count 0)
+ (run-with-idle-timer 0 nil
+ (lambda ()
+ (elfeed-db-add all-new-entries))))
+ ;; Report results for each feed
+ (dolist (result (nreverse results))
+ (message "GitHub feed %s [%s]: %d new entries"
+ (nth 0 result) (nth 1 result) (nth 2 result)))
+ ;; Defer UI update to avoid blocking
+ (run-with-idle-timer 0.2 nil
(lambda ()
- (elfeed-db-add all-new-entries))))
- ;; Report results for each feed
- (dolist (result (nreverse results))
- (message "GitHub feed %s [%s]: %d new entries"
- (nth 0 result) (nth 1 result) (nth 2 result)))
- ;; Defer UI update to avoid blocking
- (run-with-idle-timer 0.2 nil
- (lambda ()
- (when (get-buffer "*elfeed-search*")
- (with-current-buffer "*elfeed-search*"
- (elfeed-search-update--force)))))))))))))
-
- ;; Start all async fetches
- (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries
- for i from 0
- for delay = (* i elfeed-custom-github-request-delay)
- do (elfeed-custom-feeds-github--fetch-prs
- repo query display-name
- (lambda (r dn items)
- (elfeed-custom-feeds-github--prs-to-elfeed r dn items completion-callback))
- delay)))
- nil)
+ (when (get-buffer "*elfeed-search*")
+ (with-current-buffer "*elfeed-search*"
+ (elfeed-search-update--force))))))))))))
+ ;; Start all async fetches
+ (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries
+ for i from 0
+ for delay = (* i elfeed-custom-github-request-delay)
+ do (elfeed-custom-feeds-github--fetch-prs
+ repo query display-name
+ (lambda (r dn items)
+ (elfeed-custom-feeds-github--prs-to-elfeed r dn items completion-callback))
+ delay))))))
(provide 'elfeed-custom-feeds-github)
diff --git a/elfeed-custom-feeds-huggingface.el b/elfeed-custom-feeds-huggingface.el
@@ -11,7 +11,7 @@
;;; Code:
(require 'elfeed)
-(require 'url)
+(require 'elfeed-curl)
(require 'json)
(require 'cl-lib)
(require 'dom)
@@ -44,37 +44,33 @@
(mid model-id))
(run-at-time delay nil
(lambda ()
- (url-retrieve url
- (lambda (status)
- (if (plist-get status :error)
- (run-with-idle-timer 0 nil cb nil)
- (goto-char (point-min))
- (if (re-search-forward "\n\n" nil t)
- ;; Capture raw HTML for async processing
- (let ((html-text (buffer-substring-no-properties (point) (point-max))))
- ;; Process HTML parsing in idle timer to avoid blocking
- (run-with-idle-timer 0 nil
- (lambda ()
- (condition-case err
- (with-temp-buffer
- (insert html-text)
- (let* ((html (libxml-parse-html-region (point-min) (point-max)))
- (content-div (or (dom-by-class html "model-card-content")
- (dom-by-class html "prose")
- (dom-by-tag html 'article))))
- (if content-div
- (with-temp-buffer
- (dom-print (if (listp content-div)
- (car content-div)
- content-div))
- (let ((html-string (buffer-string)))
- (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string))))
- (funcall cb nil))))
- (error
- (message "Error parsing HTML for %s: %s" mid err)
- (funcall cb nil))))))
- (run-with-idle-timer 0 nil cb nil))))
- nil t)))))
+ (elfeed-curl-enqueue url
+ (lambda (status)
+ (if (not status)
+ (run-with-idle-timer 0 nil cb nil)
+ ;; Capture raw HTML for async processing
+ (let ((html-text (buffer-substring-no-properties (point-min) (point-max))))
+ ;; Process HTML parsing in idle timer to avoid blocking
+ (run-with-idle-timer 0 nil
+ (lambda ()
+ (condition-case err
+ (with-temp-buffer
+ (insert html-text)
+ (let* ((html (libxml-parse-html-region (point-min) (point-max)))
+ (content-div (or (dom-by-class html "model-card-content")
+ (dom-by-class html "prose")
+ (dom-by-tag html 'article))))
+ (if content-div
+ (with-temp-buffer
+ (dom-print (if (listp content-div)
+ (car content-div)
+ content-div))
+ (let ((html-string (buffer-string)))
+ (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string))))
+ (funcall cb nil))))
+ (error
+ (message "Error parsing HTML for %s: %s" mid err)
+ (funcall cb nil)))))))))))))
(defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback)
"Fetch models for ORG-NAME from HuggingFace API and call CALLBACK with results."
@@ -82,28 +78,25 @@
(url-hexify-string org-name)))
(cb callback)
(org org-name))
- (url-retrieve url
- (lambda (status)
- (if (plist-get status :error)
- (message "Error fetching HF org %s: %s" org (plist-get status :error))
- (goto-char (point-min))
- (re-search-forward "\n\n")
- ;; Capture JSON text for async parsing
- (let ((json-text (buffer-substring-no-properties (point) (point-max))))
- ;; Parse JSON in idle timer to avoid blocking
- (run-with-idle-timer 0 nil
- (lambda ()
- (condition-case err
- (with-temp-buffer
- (insert json-text)
- (goto-char (point-min))
- (let* ((json-object-type 'alist)
- (json-array-type 'list)
- (models (json-read)))
- (funcall cb org models)))
- (error
- (message "Error parsing JSON for HF org %s: %s" org err))))))))
- nil t)))
+ (elfeed-curl-enqueue url
+ (lambda (status)
+ (if (not status)
+ (message "Error fetching HF org %s: %s" org elfeed-curl-error-message)
+ ;; Capture JSON text for async parsing
+ (let ((json-text (buffer-substring-no-properties (point-min) (point-max))))
+ ;; Parse JSON in idle timer to avoid blocking
+ (run-with-idle-timer 0 nil
+ (lambda ()
+ (condition-case err
+ (with-temp-buffer
+ (insert json-text)
+ (goto-char (point-min))
+ (let* ((json-object-type 'alist)
+ (json-array-type 'list)
+ (models (json-read)))
+ (funcall cb org models)))
+ (error
+ (message "Error parsing JSON for HF org %s: %s" org err)))))))))))
(defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html)
"Create an elfeed entry for MODEL with model card HTML content.
@@ -154,13 +147,10 @@ Batches all entries and updates UI once at the end to avoid blocking."
(setf (elfeed-feed-url feed) feed-url
(elfeed-feed-title feed) (format "HuggingFace: %s" org-name)))))
- ;; Return immediately if no models
- (when (zerop total)
- (message "HuggingFace feed %s: no models to process" org-name)
- (cl-return-from elfeed-custom-feeds-huggingface--models-to-elfeed nil))
-
- ;; Fetch model cards asynchronously and batch entries
- (cl-loop for model in models
+ (if (zerop total)
+ (message "HuggingFace feed %s: no models to process" org-name)
+ ;; Fetch model cards asynchronously and batch entries
+ (cl-loop for model in models
for i from 0
for model-id = (alist-get 'id model)
for delay = (* i elfeed-custom-huggingface-request-delay)
@@ -190,7 +180,7 @@ Batches all entries and updates UI once at the end to avoid blocking."
(with-current-buffer "*elfeed-search*"
(elfeed-search-update--force)))))))))))
delay))
- nil))
+ nil)))
;;;###autoload
(defun elfeed-custom-feeds-huggingface-update ()