diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-28 04:06:12 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-28 04:06:12 -0400 |
| commit | ce74e30562dfac3f599a94a45ac16c3c2319ab41 (patch) | |
| tree | 0677f2538373bd87099a3c69b249b495689277f5 | |
| parent | 19e103fea2e55a3e3bc3a777b4c618e644b57760 (diff) | |
| download | emacs-wttrin-ce74e30562dfac3f599a94a45ac16c3c2319ab41.tar.gz emacs-wttrin-ce74e30562dfac3f599a94a45ac16c3c2319ab41.zip | |
fix: drop stale async weather responses that overwrite a newer query
wttrin-query reuses the single *wttr.in* buffer, so requesting Paris then Berlin
and having Paris return last overwrote the Berlin buffer. Each query now stamps
a monotonic request id on the buffer; the callback displays only when its
captured id still matches, so a superseded response is ignored.
| -rw-r--r-- | tests/test-wttrin-query-request-id.el | 56 | ||||
| -rw-r--r-- | wttrin.el | 16 |
2 files changed, 70 insertions, 2 deletions
diff --git a/tests/test-wttrin-query-request-id.el b/tests/test-wttrin-query-request-id.el new file mode 100644 index 0000000..cf3c371 --- /dev/null +++ b/tests/test-wttrin-query-request-id.el @@ -0,0 +1,56 @@ +;;; test-wttrin-query-request-id.el --- Stale async response guard -*- lexical-binding: t; -*- + +;; Copyright (C) 2024-2026 Craig Jennings + +;;; Commentary: +;; wttrin-query reuses the single *wttr.in* buffer, so an async response for an +;; earlier query that completes after a later one must not overwrite the newer +;; buffer. A per-buffer request id, captured in the callback closure, drops the +;; stale response. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'wttrin) +(require 'testutil-wttrin) + +(ert-deftest test-wttrin-query-normal-current-callback-displays () + "Normal: a query's own callback (matching request id) displays its result." + (let ((cb nil) (displayed nil)) + (unwind-protect + (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch) + (lambda (_loc c) (setq cb c))) + ((symbol-function 'switch-to-buffer) + (lambda (buf &rest _) (set-buffer buf))) + ((symbol-function 'wttrin--display-weather) + (lambda (query &rest _) (setq displayed query)))) + (wttrin-query "Paris") + (funcall cb "paris-data") + (should (equal displayed "Paris"))) + (when (get-buffer "*wttr.in*") (kill-buffer "*wttr.in*"))))) + +(ert-deftest test-wttrin-query-error-stale-callback-does-not-overwrite-newer () + "Error: a slow response for an earlier query does not overwrite a newer query. +Paris then Berlin share the *wttr.in* buffer; the Paris callback arriving after +Berlin must be ignored." + (let ((callbacks nil) (displayed nil)) + (unwind-protect + (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch) + (lambda (_loc cb) (push cb callbacks))) + ((symbol-function 'switch-to-buffer) + (lambda (buf &rest _) (set-buffer buf))) + ((symbol-function 'wttrin--display-weather) + (lambda (query &rest _) (setq displayed query)))) + (wttrin-query "Paris") + (wttrin-query "Berlin") + (let ((paris-cb (cadr callbacks)) ; first pushed + (berlin-cb (car callbacks))) ; last pushed + (funcall berlin-cb "berlin-data") + (should (equal displayed "Berlin")) + (funcall paris-cb "paris-data") + (should (equal displayed "Berlin")))) + (when (get-buffer "*wttr.in*") (kill-buffer "*wttr.in*"))))) + +(provide 'test-wttrin-query-request-id) +;;; test-wttrin-query-request-id.el ends here @@ -1216,22 +1216,34 @@ coordinates but can name the place)." (setq-local wttrin--current-address address) (wttrin--debug-mode-line-info))))) +(defvar wttrin--request-counter 0 + "Monotonic counter for weather requests, used to drop stale async responses.") + +(defvar-local wttrin--current-request-id nil + "Request id of the most recent query for this weather buffer.") + (defun wttrin-query (query &optional display address) "Asynchronously query weather for QUERY, display the result when ready. QUERY is what weather is fetched by (and the cache key). Optional DISPLAY is the name shown in the header (a saved-location name); when nil it falls back to QUERY. Optional ADDRESS is shown on a \"Location:\" line, used when QUERY is raw coordinates from a geolocation command." - (let ((buffer (get-buffer-create (format "*wttr.in*")))) + (let ((buffer (get-buffer-create (format "*wttr.in*"))) + (request-id (setq wttrin--request-counter (1+ wttrin--request-counter)))) (switch-to-buffer buffer) (setq buffer-read-only nil) (erase-buffer) (insert "Loading weather for " (or display query) "...") (setq buffer-read-only t) + (setq-local wttrin--current-request-id request-id) (wttrin--get-cached-or-fetch query (lambda (raw-string &optional error-msg) - (when (buffer-live-p buffer) + ;; The single *wttr.in* buffer is reused, so ignore a response whose + ;; query was superseded by a newer one before it returned. + (when (and (buffer-live-p buffer) + (= request-id + (buffer-local-value 'wttrin--current-request-id buffer))) (with-current-buffer buffer (wttrin--display-weather query raw-string error-msg display address))))))) |
