summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvin <git@vineetk.net>2025-12-08 17:02:56 -0500
committervin <git@vineetk.net>2025-12-08 17:02:56 -0500
commitb67d770226c84c0cd5869a352622ebbd3b0e7ba4 (patch)
treed232372d1de53e05f0aa8c0865bbf956dd25dbd3
parentaf9627b43eaf4e40fadc9be82ccc2f10f33746a3 (diff)
parse in batches and via idle timers to avoid blocking emacs
-rw-r--r--elfeed-custom-feeds-github.el161
-rw-r--r--elfeed-custom-feeds-huggingface.el103
2 files changed, 161 insertions, 103 deletions
diff --git a/elfeed-custom-feeds-github.el b/elfeed-custom-feeds-github.el
index a857963..5bfdb6c 100644
--- a/elfeed-custom-feeds-github.el
+++ b/elfeed-custom-feeds-github.el
@@ -50,58 +50,83 @@
50 (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error)) 50 (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error))
51 (goto-char (point-min)) 51 (goto-char (point-min))
52 (re-search-forward "\n\n") 52 (re-search-forward "\n\n")
53 (let* ((json-object-type 'alist) 53 ;; Capture JSON text for async parsing
54 (json-array-type 'list) 54 (let ((json-text (buffer-substring-no-properties (point) (point-max))))
55 (response (json-read)) 55 ;; Parse JSON in idle timer to avoid blocking
56 (items (alist-get 'items response))) 56 (run-with-idle-timer 0 nil
57 (funcall cb r dn items)))) 57 (lambda ()
58 (condition-case err
59 (with-temp-buffer
60 (insert json-text)
61 (goto-char (point-min))
62 (let* ((json-object-type 'alist)
63 (json-array-type 'list)
64 (response (json-read))
65 (items (alist-get 'items response)))
66 (funcall cb r dn items)))
67 (error
68 (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err)))))))
58 nil t))))) 69 nil t)))))
59 70
60(defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback) 71(defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback)
61 "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME. 72 "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME.
62Call COMPLETION-CALLBACK with (repo display-name new-entries) when done." 73Call COMPLETION-CALLBACK with (repo display-name new-entries) when done."
63 (let ((feed-url (format "https://github.com/%s/pulls" repo)) 74 (let ((feed-url (format "https://github.com/%s/pulls" repo))
64 (new-entries ())) 75 (new-entries ())
65 (let ((feed (elfeed-db-get-feed feed-url))) 76 (prs-to-process prs))
66 (setf (elfeed-feed-url feed) feed-url 77 ;; Set up feed metadata asynchronously
67 (elfeed-feed-title feed) (format "GitHub: %s [%s]" repo display-name))) 78 (run-with-idle-timer 0 nil
68 79 (lambda ()
69 (dolist (pr prs) 80 (let ((feed (elfeed-db-get-feed feed-url)))
70 (let* ((number (alist-get 'number pr)) 81 (setf (elfeed-feed-url feed) feed-url
71 (title-text (alist-get 'title pr)) 82 (elfeed-feed-title feed) (format "GitHub: %s [%s]" repo display-name)))))
72 (html-url (alist-get 'html_url pr)) 83
73 (state (alist-get 'state pr)) 84 ;; Process PRs in chunks to avoid blocking
74 (updated (alist-get 'updated_at pr)) 85 (cl-labels ((process-next-chunk ()
75 (created (alist-get 'created_at pr)) 86 (when prs-to-process
76 (user (alist-get 'login (alist-get 'user pr))) 87 ;; Process 5 PRs at a time
77 (body (or (alist-get 'body pr) "No description provided.")) 88 (let ((chunk (cl-subseq prs-to-process 0 (min 5 (length prs-to-process)))))
78 (labels (mapcar (lambda (label) (alist-get 'name label)) 89 (setq prs-to-process (cl-subseq prs-to-process (length chunk)))
79 (alist-get 'labels pr))) 90 ;; Process this chunk
80 (entry-id html-url) 91 (dolist (pr chunk)
81 (title (format "#%d: %s" number title-text)) 92 (let* ((number (alist-get 'number pr))
82 (date (float-time (date-to-time updated))) 93 (title-text (alist-get 'title pr))
83 (content (format "<h2>PR #%d: %s</h2>\n<p><strong>State:</strong> %s</p>\n<p><strong>Author:</strong> %s</p>\n<p><strong>Created:</strong> %s</p>\n<p><strong>Updated:</strong> %s</p>\n%s\n<h3>Description:</h3>\n<pre>%s</pre>" 94 (html-url (alist-get 'html_url pr))
84 number title-text state user created updated 95 (state (alist-get 'state pr))
85 (if labels 96 (updated (alist-get 'updated_at pr))
86 (format "<p><strong>Labels:</strong> %s</p>" 97 (created (alist-get 'created_at pr))
87 (mapconcat 'identity labels ", ")) 98 (user (alist-get 'login (alist-get 'user pr)))
88 "") 99 (body (or (alist-get 'body pr) "No description provided."))
89 (replace-regexp-in-string "<" "&lt;" 100 (labels (mapcar (lambda (label) (alist-get 'name label))
90 (replace-regexp-in-string ">" "&gt;" body))))) 101 (alist-get 'labels pr)))
91 (unless (elfeed-db-get-entry entry-id) 102 (entry-id html-url)
92 (let ((entry (elfeed-entry--create 103 (title (format "#%d: %s" number title-text))
93 :id entry-id 104 (date (float-time (date-to-time updated)))
94 :title title 105 (content (format "<h2>PR #%d: %s</h2>\n<p><strong>State:</strong> %s</p>\n<p><strong>Author:</strong> %s</p>\n<p><strong>Created:</strong> %s</p>\n<p><strong>Updated:</strong> %s</p>\n%s\n<h3>Description:</h3>\n<pre>%s</pre>"
95 :link html-url 106 number title-text state user created updated
96 :date date 107 (if labels
97 :content content 108 (format "<p><strong>Labels:</strong> %s</p>"
98 :content-type 'html 109 (mapconcat 'identity labels ", "))
99 :feed-id feed-url 110 "")
100 :tags '(unread github)))) 111 (replace-regexp-in-string "<" "&lt;"
101 (push entry new-entries))))) 112 (replace-regexp-in-string ">" "&gt;" body)))))
102 113 (unless (elfeed-db-get-entry entry-id)
103 ;; Call completion callback with results 114 (let ((entry (elfeed-entry--create
104 (funcall completion-callback repo display-name (nreverse new-entries)))) 115 :id entry-id
116 :title title
117 :link html-url
118 :date date
119 :content content
120 :content-type 'html
121 :feed-id feed-url
122 :tags '(unread github))))
123 (push entry new-entries)))))
124 ;; Schedule next chunk or call completion
125 (if prs-to-process
126 (run-with-idle-timer 0 nil #'process-next-chunk)
127 ;; All done - call completion callback
128 (funcall completion-callback repo display-name (nreverse new-entries)))))))
129 (process-next-chunk))))
105 130
106;;;###autoload 131;;;###autoload
107(defun elfeed-custom-feeds-github-update () 132(defun elfeed-custom-feeds-github-update ()
@@ -120,26 +145,30 @@ Batches all entries and updates UI once at the end to avoid blocking."
120 ;; Completion callback that batches everything 145 ;; Completion callback that batches everything
121 (let ((completion-callback 146 (let ((completion-callback
122 (lambda (repo display-name new-entries) 147 (lambda (repo display-name new-entries)
123 (setq processed (1+ processed)) 148 (run-with-idle-timer 0 nil
124 (when new-entries 149 (lambda ()
125 (setq all-new-entries (append all-new-entries new-entries))) 150 (setq processed (1+ processed))
126 (push (list repo display-name (length new-entries)) results) 151 (when new-entries
127 152 (setq all-new-entries (append all-new-entries new-entries)))
128 ;; When all queries are done, batch add to DB and update UI once 153 (push (list repo display-name (length new-entries)) results)
129 (when (= processed total) 154
130 (let ((total-count (length all-new-entries))) 155 ;; When all queries are done, batch add to DB and update UI once
131 (when (> total-count 0) 156 (when (= processed total)
132 (elfeed-db-add all-new-entries)) 157 (let ((total-count (length all-new-entries)))
133 ;; Report results for each feed 158 (when (> total-count 0)
134 (dolist (result (nreverse results)) 159 (run-with-idle-timer 0 nil
135 (message "GitHub feed %s [%s]: %d new entries" 160 (lambda ()
136 (nth 0 result) (nth 1 result) (nth 2 result))) 161 (elfeed-db-add all-new-entries))))
137 ;; Defer UI update to avoid blocking 162 ;; Report results for each feed
138 (run-with-idle-timer 0.1 nil 163 (dolist (result (nreverse results))
139 (lambda () 164 (message "GitHub feed %s [%s]: %d new entries"
140 (when (get-buffer "*elfeed-search*") 165 (nth 0 result) (nth 1 result) (nth 2 result)))
141 (with-current-buffer "*elfeed-search*" 166 ;; Defer UI update to avoid blocking
142 (elfeed-search-update--force)))))))))) 167 (run-with-idle-timer 0.2 nil
168 (lambda ()
169 (when (get-buffer "*elfeed-search*")
170 (with-current-buffer "*elfeed-search*"
171 (elfeed-search-update--force)))))))))))
143 172
144 ;; Start all async fetches 173 ;; Start all async fetches
145 (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries 174 (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries
diff --git a/elfeed-custom-feeds-huggingface.el b/elfeed-custom-feeds-huggingface.el
index 43f64c8..81f4b5b 100644
--- a/elfeed-custom-feeds-huggingface.el
+++ b/elfeed-custom-feeds-huggingface.el
@@ -47,22 +47,33 @@
47 (url-retrieve url 47 (url-retrieve url
48 (lambda (status) 48 (lambda (status)
49 (if (plist-get status :error) 49 (if (plist-get status :error)
50 (funcall cb nil) 50 (run-with-idle-timer 0 nil cb nil)
51 (goto-char (point-min)) 51 (goto-char (point-min))
52 (if (re-search-forward "\n\n" nil t) 52 (if (re-search-forward "\n\n" nil t)
53 (let* ((html (libxml-parse-html-region (point) (point-max))) 53 ;; Capture raw HTML for async processing
54 (content-div (or (dom-by-class html "model-card-content") 54 (let ((html-text (buffer-substring-no-properties (point) (point-max))))
55 (dom-by-class html "prose") 55 ;; Process HTML parsing in idle timer to avoid blocking
56 (dom-by-tag html 'article)))) 56 (run-with-idle-timer 0 nil
57 (if content-div 57 (lambda ()
58 (with-temp-buffer 58 (condition-case err
59 (dom-print (if (listp content-div) 59 (with-temp-buffer
60 (car content-div) 60 (insert html-text)
61 content-div)) 61 (let* ((html (libxml-parse-html-region (point-min) (point-max)))
62 (let ((html-string (buffer-string))) 62 (content-div (or (dom-by-class html "model-card-content")
63 (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string)))) 63 (dom-by-class html "prose")
64 (funcall cb nil))) 64 (dom-by-tag html 'article))))
65 (funcall cb nil)))) 65 (if content-div
66 (with-temp-buffer
67 (dom-print (if (listp content-div)
68 (car content-div)
69 content-div))
70 (let ((html-string (buffer-string)))
71 (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string))))
72 (funcall cb nil))))
73 (error
74 (message "Error parsing HTML for %s: %s" mid err)
75 (funcall cb nil))))))
76 (run-with-idle-timer 0 nil cb nil))))
66 nil t))))) 77 nil t)))))
67 78
68(defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback) 79(defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback)
@@ -77,10 +88,21 @@
77 (message "Error fetching HF org %s: %s" org (plist-get status :error)) 88 (message "Error fetching HF org %s: %s" org (plist-get status :error))
78 (goto-char (point-min)) 89 (goto-char (point-min))
79 (re-search-forward "\n\n") 90 (re-search-forward "\n\n")
80 (let* ((json-object-type 'alist) 91 ;; Capture JSON text for async parsing
81 (json-array-type 'list) 92 (let ((json-text (buffer-substring-no-properties (point) (point-max))))
82 (models (json-read))) 93 ;; Parse JSON in idle timer to avoid blocking
83 (funcall cb org models)))) 94 (run-with-idle-timer 0 nil
95 (lambda ()
96 (condition-case err
97 (with-temp-buffer
98 (insert json-text)
99 (goto-char (point-min))
100 (let* ((json-object-type 'alist)
101 (json-array-type 'list)
102 (models (json-read)))
103 (funcall cb org models)))
104 (error
105 (message "Error parsing JSON for HF org %s: %s" org err))))))))
84 nil t))) 106 nil t)))
85 107
86(defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html) 108(defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html)
@@ -126,9 +148,11 @@ Batches all entries and updates UI once at the end to avoid blocking."
126 (new-entries '()) 148 (new-entries '())
127 (processed 0)) 149 (processed 0))
128 ;; Set up feed metadata 150 ;; Set up feed metadata
129 (let ((feed (elfeed-db-get-feed feed-url))) 151 (run-with-idle-timer 0 nil
130 (setf (elfeed-feed-url feed) feed-url 152 (lambda ()
131 (elfeed-feed-title feed) (format "HuggingFace: %s" org-name))) 153 (let ((feed (elfeed-db-get-feed feed-url)))
154 (setf (elfeed-feed-url feed) feed-url
155 (elfeed-feed-title feed) (format "HuggingFace: %s" org-name)))))
132 156
133 ;; Return immediately if no models 157 ;; Return immediately if no models
134 (when (zerop total) 158 (when (zerop total)
@@ -144,22 +168,27 @@ Batches all entries and updates UI once at the end to avoid blocking."
144 model-id 168 model-id
145 (let ((m model)) 169 (let ((m model))
146 (lambda (html) 170 (lambda (html)
147 (let ((entry (elfeed-custom-feeds-huggingface--create-entry org-name m html))) 171 ;; Process entry creation in idle timer to avoid blocking
148 (when entry 172 (run-with-idle-timer 0 nil
149 (push entry new-entries))) 173 (lambda ()
150 (setq processed (1+ processed)) 174 (let ((entry (elfeed-custom-feeds-huggingface--create-entry org-name m html)))
151 ;; When all are processed, batch add to DB and update UI once 175 (when entry
152 (when (= processed total) 176 (push entry new-entries)))
153 (let ((count (length new-entries))) 177 (setq processed (1+ processed))
154 (when (> count 0) 178 ;; When all are processed, batch add to DB and update UI once
155 (elfeed-db-add (nreverse new-entries))) 179 (when (= processed total)
156 (message "HuggingFace feed %s: %d new entries" org-name count) 180 (let ((count (length new-entries)))
157 ;; Defer UI update to avoid blocking 181 (when (> count 0)
158 (run-with-idle-timer 0.1 nil 182 (run-with-idle-timer 0 nil
159 (lambda () 183 (lambda ()
160 (when (get-buffer "*elfeed-search*") 184 (elfeed-db-add (nreverse new-entries)))))
161 (with-current-buffer "*elfeed-search*" 185 (message "HuggingFace feed %s: %d new entries" org-name count)
162 (elfeed-search-update--force))))))))) 186 ;; Defer UI update to avoid blocking
187 (run-with-idle-timer 0.2 nil
188 (lambda ()
189 (when (get-buffer "*elfeed-search*")
190 (with-current-buffer "*elfeed-search*"
191 (elfeed-search-update--force)))))))))))
163 delay)) 192 delay))
164 nil)) 193 nil))
165 194